feat(smart-app): implement complete mobile app MVP

- App.tsx: full navigation (Auth stack + Main tabs with 5 screens)
- Auth: LoginScreen, RegisterScreen, ForgotPasswordScreen
- HomeScreen: dashboard with IoT metrics, weather widget, alerts, quick actions, sensors
- MapScreen: interactive map with layer toggles (6 layers)
- MarketplaceScreen: categories (6), products (5), search
- ChatScreen: AI chat with quick prompts (4), bot responses
- ProfileScreen: user info, stats, menu (9 items), logout
- AlertsScreen: alert list with severity, acknowledge
- SensorsScreen: sensor list with type filters (6 types), search
- ZonesScreen: zone cards with stats
- SettingsScreen: language picker (FR/EN/ES/DE), privacy, about
- Stores: iotStore (sensors, zones, alerts), notificationStore, uiStore + i18n
- Hooks: useSensors, useAlerts, useNotifications, useLocation
- Components: Card, Button, LoadingSpinner, ErrorBoundary, Header
- Services: iotService, notificationService (with axios API client)
- Utils: formatters (temp, AQI, noise, dates), validators (email, password, IBAN)
- Theme: colors.ts with full design system (Blue Ocean palette)
- Ditto: fixed MongoDB connection, new JWT secrets, official gateway image
This commit is contained in:
Eric FELIXINE
2026-06-01 18:00:35 -04:00
parent 08ca495bde
commit e30ae8ed09
35578 changed files with 3703534 additions and 43 deletions

View File

@@ -0,0 +1,9 @@
export const ActionType = {
REANIMATED_WORKLET: 1,
NATIVE_ANIMATED_EVENT: 2,
JS_FUNCTION_OLD_API: 3,
JS_FUNCTION_NEW_API: 4,
} as const;
// eslint-disable-next-line @typescript-eslint/no-redeclare -- backward compatibility; it can be used as a type and as a value
export type ActionType = typeof ActionType[keyof typeof ActionType];

View File

@@ -0,0 +1,26 @@
const RIGHT = 1;
const LEFT = 2;
const UP = 4;
const DOWN = 8;
// public interface
export const Directions = {
RIGHT: RIGHT,
LEFT: LEFT,
UP: UP,
DOWN: DOWN,
} as const;
// internal interface
export const DiagonalDirections = {
UP_RIGHT: UP | RIGHT,
DOWN_RIGHT: DOWN | RIGHT,
UP_LEFT: UP | LEFT,
DOWN_LEFT: DOWN | LEFT,
} as const;
// eslint-disable-next-line @typescript-eslint/no-redeclare -- backward compatibility; it can be used as a type and as a value
export type Directions = typeof Directions[keyof typeof Directions];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export type DiagonalDirections =
typeof DiagonalDirections[keyof typeof DiagonalDirections];

View File

@@ -0,0 +1,35 @@
import { Platform } from 'react-native';
let useNewWebImplementation = true;
let getWasCalled = false;
export function enableExperimentalWebImplementation(
_shouldEnable = true
): void {
// NO-OP since the new implementation is now the default
}
export function enableLegacyWebImplementation(
shouldUseLegacyImplementation = true
): void {
if (
Platform.OS !== 'web' ||
useNewWebImplementation === !shouldUseLegacyImplementation
) {
return;
}
if (getWasCalled) {
console.error(
'Some parts of this application have already started using the new gesture handler implementation. No changes will be applied. You can try enabling legacy implementation earlier.'
);
return;
}
useNewWebImplementation = !shouldUseLegacyImplementation;
}
export function isNewWebImplementationEnabled(): boolean {
getWasCalled = true;
return useNewWebImplementation;
}

View File

@@ -0,0 +1,3 @@
import React from 'react';
export default React.createContext(false);

View File

@@ -0,0 +1,8 @@
import { NativeModules, Platform } from 'react-native';
type PlatformConstants = {
forceTouchAvailable: boolean;
};
export default (NativeModules?.PlatformConstants ??
Platform.constants) as PlatformConstants;

View File

@@ -0,0 +1,5 @@
export default {
get forceTouchAvailable() {
return false;
},
};

View File

@@ -0,0 +1,6 @@
export enum PointerType {
TOUCH,
STYLUS,
MOUSE,
OTHER,
}

View File

@@ -0,0 +1,5 @@
// Reexport the native module spec used by codegen. The relevant files are inluded on Android
// to ensure the compatibility with the old arch, while iOS doesn't require those at all.
import Module from './specs/NativeRNGestureHandlerModule';
export default Module;

View File

@@ -0,0 +1,105 @@
import React from 'react';
import type { ActionType } from './ActionType';
import { isNewWebImplementationEnabled } from './EnableNewWebImplementation';
import { Gestures, HammerGestures } from './web/Gestures';
import type { Config } from './web/interfaces';
import InteractionManager from './web/tools/InteractionManager';
import NodeManager from './web/tools/NodeManager';
import * as HammerNodeManager from './web_hammer/NodeManager';
import { GestureHandlerWebDelegate } from './web/tools/GestureHandlerWebDelegate';
export default {
handleSetJSResponder(tag: number, blockNativeResponder: boolean) {
console.warn('handleSetJSResponder: ', tag, blockNativeResponder);
},
handleClearJSResponder() {
console.warn('handleClearJSResponder: ');
},
createGestureHandler<T>(
handlerName: keyof typeof Gestures,
handlerTag: number,
config: T
) {
if (isNewWebImplementationEnabled()) {
if (!(handlerName in Gestures)) {
throw new Error(
`react-native-gesture-handler: ${handlerName} is not supported on web.`
);
}
const GestureClass = Gestures[handlerName];
NodeManager.createGestureHandler(
handlerTag,
new GestureClass(new GestureHandlerWebDelegate())
);
InteractionManager.getInstance().configureInteractions(
NodeManager.getHandler(handlerTag),
config as unknown as Config
);
} else {
if (!(handlerName in HammerGestures)) {
throw new Error(
`react-native-gesture-handler: ${handlerName} is not supported on web.`
);
}
// @ts-ignore If it doesn't exist, the error is thrown
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const GestureClass = HammerGestures[handlerName];
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
HammerNodeManager.createGestureHandler(handlerTag, new GestureClass());
}
this.updateGestureHandler(handlerTag, config as unknown as Config);
},
attachGestureHandler(
handlerTag: number,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
newView: any,
_actionType: ActionType,
propsRef: React.RefObject<unknown>
) {
if (
!(newView instanceof HTMLElement || newView instanceof React.Component)
) {
return;
}
if (isNewWebImplementationEnabled()) {
//@ts-ignore Types should be HTMLElement or React.Component
NodeManager.getHandler(handlerTag).init(newView, propsRef);
} else {
//@ts-ignore Types should be HTMLElement or React.Component
HammerNodeManager.getHandler(handlerTag).setView(newView, propsRef);
}
},
updateGestureHandler(handlerTag: number, newConfig: Config) {
if (isNewWebImplementationEnabled()) {
NodeManager.getHandler(handlerTag).updateGestureConfig(newConfig);
InteractionManager.getInstance().configureInteractions(
NodeManager.getHandler(handlerTag),
newConfig
);
} else {
HammerNodeManager.getHandler(handlerTag).updateGestureConfig(newConfig);
}
},
getGestureHandlerNode(handlerTag: number) {
if (isNewWebImplementationEnabled()) {
return NodeManager.getHandler(handlerTag);
} else {
return HammerNodeManager.getHandler(handlerTag);
}
},
dropGestureHandler(handlerTag: number) {
if (isNewWebImplementationEnabled()) {
NodeManager.dropGestureHandler(handlerTag);
} else {
HammerNodeManager.dropGestureHandler(handlerTag);
}
},
// eslint-disable-next-line @typescript-eslint/no-empty-function
flushOperations() {},
};

View File

@@ -0,0 +1,62 @@
import React from 'react';
import { ActionType } from './ActionType';
// GestureHandlers
import PanGestureHandler from './web/handlers/PanGestureHandler';
import TapGestureHandler from './web/handlers/TapGestureHandler';
import LongPressGestureHandler from './web/handlers/LongPressGestureHandler';
import PinchGestureHandler from './web/handlers/PinchGestureHandler';
import RotationGestureHandler from './web/handlers/RotationGestureHandler';
import FlingGestureHandler from './web/handlers/FlingGestureHandler';
import NativeViewGestureHandler from './web/handlers/NativeViewGestureHandler';
import ManualGestureHandler from './web/handlers/ManualGestureHandler';
import { Config } from './web/interfaces';
export const Gestures = {
NativeViewGestureHandler,
PanGestureHandler,
TapGestureHandler,
LongPressGestureHandler,
PinchGestureHandler,
RotationGestureHandler,
FlingGestureHandler,
ManualGestureHandler,
};
export default {
handleSetJSResponder(_tag: number, _blockNativeResponder: boolean) {
// NO-OP
},
handleClearJSResponder() {
// NO-OP
},
createGestureHandler<T>(
_handlerName: keyof typeof Gestures,
_handlerTag: number,
_config: T
) {
// NO-OP
},
attachGestureHandler(
_handlerTag: number,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
_newView: any,
_actionType: ActionType,
_propsRef: React.RefObject<unknown>
) {
// NO-OP
},
updateGestureHandler(_handlerTag: number, _newConfig: Config) {
// NO-OP
},
getGestureHandlerNode(_handlerTag: number) {
// NO-OP
},
dropGestureHandler(_handlerTag: number) {
// NO-OP
},
flushOperations() {
// NO-OP
},
};

View File

@@ -0,0 +1,3 @@
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-nocheck
export { default as RNRenderer } from 'react-native/Libraries/Renderer/shims/ReactNative';

View File

@@ -0,0 +1,3 @@
export const RNRenderer = {
findHostInstance_DEPRECATED: (_ref: any) => null,
};

View File

@@ -0,0 +1,13 @@
// TODO use State from RNModule
export const State = {
UNDETERMINED: 0,
FAILED: 1,
BEGAN: 2,
CANCELLED: 3,
ACTIVE: 4,
END: 5,
} as const;
// eslint-disable-next-line @typescript-eslint/no-redeclare -- backward compatibility; it can be used as a type and as a value
export type State = typeof State[keyof typeof State];

View File

@@ -0,0 +1,10 @@
export const TouchEventType = {
UNDETERMINED: 0,
TOUCHES_DOWN: 1,
TOUCHES_MOVE: 2,
TOUCHES_UP: 3,
TOUCHES_CANCELLED: 4,
} as const;
// eslint-disable-next-line @typescript-eslint/no-redeclare -- backward compatibility; it can be used as a type and as a value
export type TouchEventType = typeof TouchEventType[keyof typeof TouchEventType];

View File

@@ -0,0 +1,758 @@
// This component is based on RN's DrawerLayoutAndroid API
//
// It perhaps deserves to be put in a separate repo, but since it relies on
// react-native-gesture-handler library which isn't very popular at the moment I
// decided to keep it here for the time being. It will allow us to move faster
// and fix issues that may arise in gesture handler library that could be found
// when using the drawer component
import * as React from 'react';
import { Component } from 'react';
import invariant from 'invariant';
import {
Animated,
StyleSheet,
View,
Keyboard,
StatusBar,
I18nManager,
StatusBarAnimation,
StyleProp,
ViewStyle,
LayoutChangeEvent,
NativeSyntheticEvent,
} from 'react-native';
import {
GestureEvent,
HandlerStateChangeEvent,
UserSelect,
ActiveCursor,
MouseButton,
} from '../handlers/gestureHandlerCommon';
import {
PanGestureHandler,
PanGestureHandlerEventPayload,
} from '../handlers/PanGestureHandler';
import {
TapGestureHandler,
TapGestureHandlerEventPayload,
} from '../handlers/TapGestureHandler';
import { State } from '../State';
const DRAG_TOSS = 0.05;
const IDLE: DrawerState = 'Idle';
const DRAGGING: DrawerState = 'Dragging';
const SETTLING: DrawerState = 'Settling';
export type DrawerPosition = 'left' | 'right';
export type DrawerState = 'Idle' | 'Dragging' | 'Settling';
export type DrawerType = 'front' | 'back' | 'slide';
export type DrawerLockMode = 'unlocked' | 'locked-closed' | 'locked-open';
export type DrawerKeyboardDismissMode = 'none' | 'on-drag';
// Animated.AnimatedInterpolation has been converted to a generic type
// in @types/react-native 0.70. This way we can maintain compatibility
// with all versions of @types/react-native`
type AnimatedInterpolation = ReturnType<Animated.Value['interpolate']>;
export interface DrawerLayoutProps {
/**
* This attribute is present in the standard implementation already and is one
* of the required params. Gesture handler version of DrawerLayout make it
* possible for the function passed as `renderNavigationView` to take an
* Animated value as a parameter that indicates the progress of drawer
* opening/closing animation (progress value is 0 when closed and 1 when
* opened). This can be used by the drawer component to animated its children
* while the drawer is opening or closing.
*/
renderNavigationView: (
progressAnimatedValue: Animated.Value
) => React.ReactNode;
drawerPosition?: DrawerPosition;
drawerWidth?: number;
drawerBackgroundColor?: string;
drawerLockMode?: DrawerLockMode;
keyboardDismissMode?: DrawerKeyboardDismissMode;
/**
* Called when the drawer is closed.
*/
onDrawerClose?: () => void;
/**
* Called when the drawer is opened.
*/
onDrawerOpen?: () => void;
/**
* Called when the status of the drawer changes.
*/
onDrawerStateChanged?: (
newState: DrawerState,
drawerWillShow: boolean
) => void;
useNativeAnimations?: boolean;
drawerType?: DrawerType;
/**
* Defines how far from the edge of the content view the gesture should
* activate.
*/
edgeWidth?: number;
minSwipeDistance?: number;
/**
* When set to true Drawer component will use
* {@link https://reactnative.dev/docs/statusbar StatusBar} API to hide the OS
* status bar whenever the drawer is pulled or when its in an "open" state.
*/
hideStatusBar?: boolean;
/**
* @default 'slide'
*
* Can be used when hideStatusBar is set to true and will select the animation
* used for hiding/showing the status bar. See
* {@link https://reactnative.dev/docs/statusbar StatusBar} documentation for
* more details
*/
statusBarAnimation?: StatusBarAnimation;
/**
* @default black
*
* Color of a semi-transparent overlay to be displayed on top of the content
* view when drawer gets open. A solid color should be used as the opacity is
* added by the Drawer itself and the opacity of the overlay is animated (from
* 0% to 70%).
*/
overlayColor?: string;
contentContainerStyle?: StyleProp<ViewStyle>;
drawerContainerStyle?: StyleProp<ViewStyle>;
/**
* Enables two-finger gestures on supported devices, for example iPads with
* trackpads. If not enabled the gesture will require click + drag, with
* `enableTrackpadTwoFingerGesture` swiping with two fingers will also trigger
* the gesture.
*/
enableTrackpadTwoFingerGesture?: boolean;
onDrawerSlide?: (position: number) => void;
onGestureRef?: (ref: PanGestureHandler) => void;
// implicit `children` prop has been removed in @types/react^18.0.0
children?:
| React.ReactNode
| ((openValue?: AnimatedInterpolation) => React.ReactNode);
/**
* @default 'none'
* Defines which userSelect property should be used.
* Values: 'none'|'text'|'auto'
*/
userSelect?: UserSelect;
/**
* @default 'auto'
* Defines which cursor property should be used when gesture activates.
* Values: see CSS cursor values
*/
activeCursor?: ActiveCursor;
/**
* @default 'MouseButton.LEFT'
* Allows to choose which mouse button should underlying pan handler react to.
*/
mouseButton?: MouseButton;
/**
* @default 'false if MouseButton.RIGHT is specified'
* Allows to enable/disable context menu.
*/
enableContextMenu?: boolean;
}
export type DrawerLayoutState = {
dragX: Animated.Value;
touchX: Animated.Value;
drawerTranslation: Animated.Value;
containerWidth: number;
drawerState: DrawerState;
drawerOpened: boolean;
};
export type DrawerMovementOption = {
velocity?: number;
speed?: number;
};
export default class DrawerLayout extends Component<
DrawerLayoutProps,
DrawerLayoutState
> {
static defaultProps = {
drawerWidth: 200,
drawerPosition: 'left',
useNativeAnimations: true,
drawerType: 'front',
edgeWidth: 20,
minSwipeDistance: 3,
overlayColor: 'rgba(0, 0, 0, 0.7)',
drawerLockMode: 'unlocked',
enableTrackpadTwoFingerGesture: false,
};
constructor(props: DrawerLayoutProps) {
super(props);
const dragX = new Animated.Value(0);
const touchX = new Animated.Value(0);
const drawerTranslation = new Animated.Value(0);
this.state = {
dragX,
touchX,
drawerTranslation,
containerWidth: 0,
drawerState: IDLE,
drawerOpened: false,
};
this.updateAnimatedEvent(props, this.state);
}
shouldComponentUpdate(props: DrawerLayoutProps, state: DrawerLayoutState) {
if (
this.props.drawerPosition !== props.drawerPosition ||
this.props.drawerWidth !== props.drawerWidth ||
this.props.drawerType !== props.drawerType ||
this.state.containerWidth !== state.containerWidth
) {
this.updateAnimatedEvent(props, state);
}
return true;
}
private openValue?: AnimatedInterpolation;
private onGestureEvent?: (
event: GestureEvent<PanGestureHandlerEventPayload>
) => void;
private accessibilityIsModalView = React.createRef<View>();
private pointerEventsView = React.createRef<View>();
private panGestureHandler = React.createRef<PanGestureHandler | null>();
private drawerShown = false;
static positions = {
Left: 'left',
Right: 'right',
};
private updateAnimatedEvent = (
props: DrawerLayoutProps,
state: DrawerLayoutState
) => {
// Event definition is based on
const { drawerPosition, drawerWidth, drawerType } = props;
const {
dragX: dragXValue,
touchX: touchXValue,
drawerTranslation,
containerWidth,
} = state;
let dragX = dragXValue;
let touchX = touchXValue;
if (drawerPosition !== 'left') {
// Most of the code is written in a way to handle left-side drawer. In
// order to handle right-side drawer the only thing we need to do is to
// reverse events coming from gesture handler in a way they emulate
// left-side drawer gestures. E.g. dragX is simply -dragX, and touchX is
// calulcated by subtracing real touchX from the width of the container
// (such that when touch happens at the right edge the value is simply 0)
dragX = Animated.multiply(
new Animated.Value(-1),
dragXValue
) as Animated.Value; // TODO(TS): (for all "as" in this file) make sure we can map this
touchX = Animated.add(
new Animated.Value(containerWidth),
Animated.multiply(new Animated.Value(-1), touchXValue)
) as Animated.Value; // TODO(TS): make sure we can map this;
touchXValue.setValue(containerWidth);
} else {
touchXValue.setValue(0);
}
// While closing the drawer when user starts gesture outside of its area (in greyed
// out part of the window), we want the drawer to follow only once finger reaches the
// edge of the drawer.
// E.g. on the diagram below drawer is illustrate by X signs and the greyed out area by
// dots. The touch gesture starts at '*' and moves left, touch path is indicated by
// an arrow pointing left
// 1) +---------------+ 2) +---------------+ 3) +---------------+ 4) +---------------+
// |XXXXXXXX|......| |XXXXXXXX|......| |XXXXXXXX|......| |XXXXX|.........|
// |XXXXXXXX|......| |XXXXXXXX|......| |XXXXXXXX|......| |XXXXX|.........|
// |XXXXXXXX|......| |XXXXXXXX|......| |XXXXXXXX|......| |XXXXX|.........|
// |XXXXXXXX|......| |XXXXXXXX|.<-*..| |XXXXXXXX|<--*..| |XXXXX|<-----*..|
// |XXXXXXXX|......| |XXXXXXXX|......| |XXXXXXXX|......| |XXXXX|.........|
// |XXXXXXXX|......| |XXXXXXXX|......| |XXXXXXXX|......| |XXXXX|.........|
// |XXXXXXXX|......| |XXXXXXXX|......| |XXXXXXXX|......| |XXXXX|.........|
// +---------------+ +---------------+ +---------------+ +---------------+
//
// For the above to work properly we define animated value that will keep
// start position of the gesture. Then we use that value to calculate how
// much we need to subtract from the dragX. If the gesture started on the
// greyed out area we take the distance from the edge of the drawer to the
// start position. Otherwise we don't subtract at all and the drawer be
// pulled back as soon as you start the pan.
//
// This is used only when drawerType is "front"
//
let translationX = dragX;
if (drawerType === 'front') {
const startPositionX = Animated.add(
touchX,
Animated.multiply(new Animated.Value(-1), dragX)
);
const dragOffsetFromOnStartPosition = startPositionX.interpolate({
inputRange: [drawerWidth! - 1, drawerWidth!, drawerWidth! + 1],
outputRange: [0, 0, 1],
});
translationX = Animated.add(
dragX,
dragOffsetFromOnStartPosition
) as Animated.Value; // TODO: as above
}
this.openValue = Animated.add(translationX, drawerTranslation).interpolate({
inputRange: [0, drawerWidth!],
outputRange: [0, 1],
extrapolate: 'clamp',
});
const gestureOptions: {
useNativeDriver: boolean;
// TODO: make sure it is correct
listener?: (
ev: NativeSyntheticEvent<PanGestureHandlerEventPayload>
) => void;
} = {
useNativeDriver: props.useNativeAnimations!,
};
if (this.props.onDrawerSlide) {
gestureOptions.listener = (ev) => {
const translationX = Math.floor(Math.abs(ev.nativeEvent.translationX));
const position = translationX / this.state.containerWidth;
this.props.onDrawerSlide?.(position);
};
}
this.onGestureEvent = Animated.event(
[{ nativeEvent: { translationX: dragXValue, x: touchXValue } }],
gestureOptions
);
};
private handleContainerLayout = ({ nativeEvent }: LayoutChangeEvent) => {
this.setState({ containerWidth: nativeEvent.layout.width });
};
private emitStateChanged = (
newState: DrawerState,
drawerWillShow: boolean
) => {
this.props.onDrawerStateChanged?.(newState, drawerWillShow);
};
private openingHandlerStateChange = ({
nativeEvent,
}: HandlerStateChangeEvent<PanGestureHandlerEventPayload>) => {
if (nativeEvent.oldState === State.ACTIVE) {
this.handleRelease({ nativeEvent });
} else if (nativeEvent.state === State.ACTIVE) {
this.emitStateChanged(DRAGGING, false);
this.setState({ drawerState: DRAGGING });
if (this.props.keyboardDismissMode === 'on-drag') {
Keyboard.dismiss();
}
if (this.props.hideStatusBar) {
StatusBar.setHidden(true, this.props.statusBarAnimation || 'slide');
}
}
};
private onTapHandlerStateChange = ({
nativeEvent,
}: HandlerStateChangeEvent<TapGestureHandlerEventPayload>) => {
if (
this.drawerShown &&
nativeEvent.oldState === State.ACTIVE &&
this.props.drawerLockMode !== 'locked-open'
) {
this.closeDrawer();
}
};
private handleRelease = ({
nativeEvent,
}: HandlerStateChangeEvent<PanGestureHandlerEventPayload>) => {
const { drawerWidth, drawerPosition, drawerType } = this.props;
const { containerWidth } = this.state;
let { translationX: dragX, velocityX, x: touchX } = nativeEvent;
if (drawerPosition !== 'left') {
// See description in _updateAnimatedEvent about why events are flipped
// for right-side drawer
dragX = -dragX;
touchX = containerWidth - touchX;
velocityX = -velocityX;
}
const gestureStartX = touchX - dragX;
let dragOffsetBasedOnStart = 0;
if (drawerType === 'front') {
dragOffsetBasedOnStart =
gestureStartX > drawerWidth! ? gestureStartX - drawerWidth! : 0;
}
const startOffsetX =
dragX + dragOffsetBasedOnStart + (this.drawerShown ? drawerWidth! : 0);
const projOffsetX = startOffsetX + DRAG_TOSS * velocityX;
const shouldOpen = projOffsetX > drawerWidth! / 2;
if (shouldOpen) {
this.animateDrawer(startOffsetX, drawerWidth!, velocityX);
} else {
this.animateDrawer(startOffsetX, 0, velocityX);
}
};
private updateShowing = (showing: boolean) => {
this.drawerShown = showing;
this.accessibilityIsModalView.current?.setNativeProps({
accessibilityViewIsModal: showing,
});
this.pointerEventsView.current?.setNativeProps({
pointerEvents: showing ? 'auto' : 'none',
});
const { drawerPosition, minSwipeDistance, edgeWidth } = this.props;
const fromLeft = drawerPosition === 'left';
// gestureOrientation is 1 if the expected gesture is from left to right and
// -1 otherwise e.g. when drawer is on the left and is closed we expect left
// to right gesture, thus orientation will be 1.
const gestureOrientation =
(fromLeft ? 1 : -1) * (this.drawerShown ? -1 : 1);
// When drawer is closed we want the hitSlop to be horizontally shorter than
// the container size by the value of SLOP. This will make it only activate
// when gesture happens not further than SLOP away from the edge
const hitSlop = fromLeft
? { left: 0, width: showing ? undefined : edgeWidth }
: { right: 0, width: showing ? undefined : edgeWidth };
// @ts-ignore internal API, maybe could be fixed in handler types
this.panGestureHandler.current?.setNativeProps({
hitSlop,
activeOffsetX: gestureOrientation * minSwipeDistance!,
});
};
private animateDrawer = (
fromValue: number | null | undefined,
toValue: number,
velocity: number,
speed?: number
) => {
this.state.dragX.setValue(0);
this.state.touchX.setValue(
this.props.drawerPosition === 'left' ? 0 : this.state.containerWidth
);
if (fromValue != null) {
let nextFramePosition = fromValue;
if (this.props.useNativeAnimations) {
// When using native driver, we predict the next position of the
// animation because it takes one frame of a roundtrip to pass RELEASE
// event from native driver to JS before we can start animating. Without
// it, it is more noticable that the frame is dropped.
if (fromValue < toValue && velocity > 0) {
nextFramePosition = Math.min(fromValue + velocity / 60.0, toValue);
} else if (fromValue > toValue && velocity < 0) {
nextFramePosition = Math.max(fromValue + velocity / 60.0, toValue);
}
}
this.state.drawerTranslation.setValue(nextFramePosition);
}
const willShow = toValue !== 0;
this.updateShowing(willShow);
this.emitStateChanged(SETTLING, willShow);
this.setState({ drawerState: SETTLING });
if (this.props.hideStatusBar) {
StatusBar.setHidden(willShow, this.props.statusBarAnimation || 'slide');
}
Animated.spring(this.state.drawerTranslation, {
velocity,
bounciness: 0,
toValue,
useNativeDriver: this.props.useNativeAnimations!,
speed: speed ?? undefined,
}).start(({ finished }) => {
if (finished) {
this.emitStateChanged(IDLE, willShow);
this.setState({ drawerOpened: willShow });
if (this.state.drawerState !== DRAGGING) {
// it's possilbe that user started drag while the drawer
// was settling, don't override state in this case
this.setState({ drawerState: IDLE });
}
if (willShow) {
this.props.onDrawerOpen?.();
} else {
this.props.onDrawerClose?.();
}
}
});
};
openDrawer = (options: DrawerMovementOption = {}) => {
this.animateDrawer(
// TODO: decide if it should be null or undefined is the proper value
undefined,
this.props.drawerWidth!,
options.velocity ? options.velocity : 0,
options.speed
);
// We need to force the update, otherwise the overlay is not rerendered and
// it would not be clickable
this.forceUpdate();
};
closeDrawer = (options: DrawerMovementOption = {}) => {
// TODO: decide if it should be null or undefined is the proper value
this.animateDrawer(
undefined,
0,
options.velocity ? options.velocity : 0,
options.speed
);
// We need to force the update, otherwise the overlay is not rerendered and
// it would be still clickable
this.forceUpdate();
};
private renderOverlay = () => {
/* Overlay styles */
invariant(this.openValue, 'should be set');
let overlayOpacity;
if (this.state.drawerState !== IDLE) {
overlayOpacity = this.openValue;
} else {
overlayOpacity = this.state.drawerOpened ? 1 : 0;
}
const dynamicOverlayStyles = {
opacity: overlayOpacity,
backgroundColor: this.props.overlayColor,
};
return (
<TapGestureHandler onHandlerStateChange={this.onTapHandlerStateChange}>
<Animated.View
pointerEvents={this.drawerShown ? 'auto' : 'none'}
ref={this.pointerEventsView}
style={[styles.overlay, dynamicOverlayStyles]}
/>
</TapGestureHandler>
);
};
private renderDrawer = () => {
const {
drawerBackgroundColor,
drawerWidth,
drawerPosition,
drawerType,
drawerContainerStyle,
contentContainerStyle,
} = this.props;
const fromLeft = drawerPosition === 'left';
const drawerSlide = drawerType !== 'back';
const containerSlide = drawerType !== 'front';
// we rely on row and row-reverse flex directions to position the drawer
// properly. Apparently for RTL these are flipped which requires us to use
// the opposite setting for the drawer to appear from left or right
// according to the drawerPosition prop
const reverseContentDirection = I18nManager.isRTL ? fromLeft : !fromLeft;
const dynamicDrawerStyles = {
backgroundColor: drawerBackgroundColor,
width: drawerWidth,
};
const openValue = this.openValue;
invariant(openValue, 'should be set');
let containerStyles;
if (containerSlide) {
const containerTranslateX = openValue.interpolate({
inputRange: [0, 1],
outputRange: fromLeft ? [0, drawerWidth!] : [0, -drawerWidth!],
extrapolate: 'clamp',
});
containerStyles = {
transform: [{ translateX: containerTranslateX }],
};
}
let drawerTranslateX: number | AnimatedInterpolation = 0;
if (drawerSlide) {
const closedDrawerOffset = fromLeft ? -drawerWidth! : drawerWidth!;
if (this.state.drawerState !== IDLE) {
drawerTranslateX = openValue.interpolate({
inputRange: [0, 1],
outputRange: [closedDrawerOffset, 0],
extrapolate: 'clamp',
});
} else {
drawerTranslateX = this.state.drawerOpened ? 0 : closedDrawerOffset;
}
}
const drawerStyles: {
transform: { translateX: number | AnimatedInterpolation }[];
flexDirection: 'row-reverse' | 'row';
} = {
transform: [{ translateX: drawerTranslateX }],
flexDirection: reverseContentDirection ? 'row-reverse' : 'row',
};
return (
<Animated.View style={styles.main} onLayout={this.handleContainerLayout}>
<Animated.View
style={[
drawerType === 'front'
? styles.containerOnBack
: styles.containerInFront,
containerStyles,
contentContainerStyle,
]}
importantForAccessibility={
this.drawerShown ? 'no-hide-descendants' : 'yes'
}>
{typeof this.props.children === 'function'
? this.props.children(this.openValue)
: this.props.children}
{this.renderOverlay()}
</Animated.View>
<Animated.View
pointerEvents="box-none"
ref={this.accessibilityIsModalView}
accessibilityViewIsModal={this.drawerShown}
style={[styles.drawerContainer, drawerStyles, drawerContainerStyle]}>
<View style={dynamicDrawerStyles}>
{this.props.renderNavigationView(this.openValue as Animated.Value)}
</View>
</Animated.View>
</Animated.View>
);
};
private setPanGestureRef = (ref: PanGestureHandler) => {
// TODO(TS): make sure it is OK taken from
// https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31065#issuecomment-596081842
(
this.panGestureHandler as React.MutableRefObject<PanGestureHandler>
).current = ref;
this.props.onGestureRef?.(ref);
};
render() {
const { drawerPosition, drawerLockMode, edgeWidth, minSwipeDistance } =
this.props;
const fromLeft = drawerPosition === 'left';
// gestureOrientation is 1 if the expected gesture is from left to right and
// -1 otherwise e.g. when drawer is on the left and is closed we expect left
// to right gesture, thus orientation will be 1.
const gestureOrientation =
(fromLeft ? 1 : -1) * (this.drawerShown ? -1 : 1);
// When drawer is closed we want the hitSlop to be horizontally shorter than
// the container size by the value of SLOP. This will make it only activate
// when gesture happens not further than SLOP away from the edge
const hitSlop = fromLeft
? { left: 0, width: this.drawerShown ? undefined : edgeWidth }
: { right: 0, width: this.drawerShown ? undefined : edgeWidth };
return (
<PanGestureHandler
// @ts-ignore could be fixed in handler types
userSelect={this.props.userSelect}
activeCursor={this.props.activeCursor}
mouseButton={this.props.mouseButton}
enableContextMenu={this.props.enableContextMenu}
ref={this.setPanGestureRef}
hitSlop={hitSlop}
activeOffsetX={gestureOrientation * minSwipeDistance!}
failOffsetY={[-15, 15]}
onGestureEvent={this.onGestureEvent}
onHandlerStateChange={this.openingHandlerStateChange}
enableTrackpadTwoFingerGesture={
this.props.enableTrackpadTwoFingerGesture
}
enabled={
drawerLockMode !== 'locked-closed' && drawerLockMode !== 'locked-open'
}>
{this.renderDrawer()}
</PanGestureHandler>
);
}
}
const styles = StyleSheet.create({
drawerContainer: {
...StyleSheet.absoluteFillObject,
zIndex: 1001,
flexDirection: 'row',
},
containerInFront: {
...StyleSheet.absoluteFillObject,
zIndex: 1002,
},
containerOnBack: {
...StyleSheet.absoluteFillObject,
},
main: {
flex: 1,
zIndex: 0,
overflow: 'hidden',
},
overlay: {
...StyleSheet.absoluteFillObject,
zIndex: 1000,
},
});

View File

@@ -0,0 +1,332 @@
import * as React from 'react';
import {
Animated,
Platform,
processColor,
StyleSheet,
StyleProp,
ViewStyle,
} from 'react-native';
import createNativeWrapper from '../handlers/createNativeWrapper';
import GestureHandlerButton from './GestureHandlerButton';
import { State } from '../State';
import {
GestureEvent,
HandlerStateChangeEvent,
} from '../handlers/gestureHandlerCommon';
import {
NativeViewGestureHandlerPayload,
NativeViewGestureHandlerProps,
} from '../handlers/NativeViewGestureHandler';
export interface RawButtonProps extends NativeViewGestureHandlerProps {
/**
* Defines if more than one button could be pressed simultaneously. By default
* set true.
*/
exclusive?: boolean;
// TODO: we should transform props in `createNativeWrapper`
/**
* Android only.
*
* Defines color of native ripple animation used since API level 21.
*/
rippleColor?: any; // it was present in BaseButtonProps before but is used here in code
/**
* Android only.
*
* Defines radius of native ripple animation used since API level 21.
*/
rippleRadius?: number | null;
/**
* Android only.
*
* Set this to true if you want the ripple animation to render outside the view bounds.
*/
borderless?: boolean;
/**
* Android only.
*
* Defines whether the ripple animation should be drawn on the foreground of the view.
*/
foreground?: boolean;
/**
* Android only.
*
* Set this to true if you don't want the system to play sound when the button is pressed.
*/
touchSoundDisabled?: boolean;
}
export interface BaseButtonProps extends RawButtonProps {
/**
* Called when the button gets pressed (analogous to `onPress` in
* `TouchableHighlight` from RN core).
*/
onPress?: (pointerInside: boolean) => void;
/**
* Called when the button gets pressed and is held for `delayLongPress`
* milliseconds.
*/
onLongPress?: () => void;
/**
* Called when button changes from inactive to active and vice versa. It
* passes active state as a boolean variable as a first parameter for that
* method.
*/
onActiveStateChange?: (active: boolean) => void;
style?: StyleProp<ViewStyle>;
testID?: string;
/**
* Delay, in milliseconds, after which the `onLongPress` callback gets called.
* Defaults to 600.
*/
delayLongPress?: number;
}
export interface RectButtonProps extends BaseButtonProps {
/**
* Background color that will be dimmed when button is in active state.
*/
underlayColor?: string;
/**
* iOS only.
*
* Opacity applied to the underlay when button is in active state.
*/
activeOpacity?: number;
}
export interface BorderlessButtonProps extends BaseButtonProps {
/**
* iOS only.
*
* Opacity applied to the button when it is in an active state.
*/
activeOpacity?: number;
}
export const RawButton = createNativeWrapper(GestureHandlerButton, {
shouldCancelWhenOutside: false,
shouldActivateOnStart: false,
});
export class BaseButton extends React.Component<BaseButtonProps> {
static defaultProps = {
delayLongPress: 600,
};
private lastActive: boolean;
private longPressTimeout: ReturnType<typeof setTimeout> | undefined;
private longPressDetected: boolean;
constructor(props: BaseButtonProps) {
super(props);
this.lastActive = false;
this.longPressDetected = false;
}
private handleEvent = ({
nativeEvent,
}: HandlerStateChangeEvent<NativeViewGestureHandlerPayload>) => {
const { state, oldState, pointerInside } = nativeEvent;
const active = pointerInside && state === State.ACTIVE;
if (active !== this.lastActive && this.props.onActiveStateChange) {
this.props.onActiveStateChange(active);
}
if (
!this.longPressDetected &&
oldState === State.ACTIVE &&
state !== State.CANCELLED &&
this.lastActive &&
this.props.onPress
) {
this.props.onPress(active);
}
if (
!this.lastActive &&
// NativeViewGestureHandler sends different events based on platform
state === (Platform.OS !== 'android' ? State.ACTIVE : State.BEGAN) &&
pointerInside
) {
this.longPressDetected = false;
if (this.props.onLongPress) {
this.longPressTimeout = setTimeout(
this.onLongPress,
this.props.delayLongPress
);
}
} else if (
// cancel longpress timeout if it's set and the finger moved out of the view
state === State.ACTIVE &&
!pointerInside &&
this.longPressTimeout !== undefined
) {
clearTimeout(this.longPressTimeout);
this.longPressTimeout = undefined;
} else if (
// cancel longpress timeout if it's set and the gesture has finished
this.longPressTimeout !== undefined &&
(state === State.END ||
state === State.CANCELLED ||
state === State.FAILED)
) {
clearTimeout(this.longPressTimeout);
this.longPressTimeout = undefined;
}
this.lastActive = active;
};
private onLongPress = () => {
this.longPressDetected = true;
this.props.onLongPress?.();
};
// Normally, the parent would execute it's handler first, then forward the
// event to listeners. However, here our handler is virtually only forwarding
// events to listeners, so we reverse the order to keep the proper order of
// the callbacks (from "raw" ones to "processed").
private onHandlerStateChange = (
e: HandlerStateChangeEvent<NativeViewGestureHandlerPayload>
) => {
this.props.onHandlerStateChange?.(e);
this.handleEvent(e);
};
private onGestureEvent = (
e: GestureEvent<NativeViewGestureHandlerPayload>
) => {
this.props.onGestureEvent?.(e);
this.handleEvent(
e as HandlerStateChangeEvent<NativeViewGestureHandlerPayload>
); // TODO: maybe it is not correct
};
render() {
const { rippleColor, ...rest } = this.props;
return (
<RawButton
rippleColor={processColor(rippleColor)}
{...rest}
onGestureEvent={this.onGestureEvent}
onHandlerStateChange={this.onHandlerStateChange}
/>
);
}
}
const AnimatedBaseButton = Animated.createAnimatedComponent(BaseButton);
const btnStyles = StyleSheet.create({
underlay: {
position: 'absolute',
left: 0,
right: 0,
bottom: 0,
top: 0,
},
});
export class RectButton extends React.Component<RectButtonProps> {
static defaultProps = {
activeOpacity: 0.105,
underlayColor: 'black',
};
private opacity: Animated.Value;
constructor(props: RectButtonProps) {
super(props);
this.opacity = new Animated.Value(0);
}
private onActiveStateChange = (active: boolean) => {
if (Platform.OS !== 'android') {
this.opacity.setValue(active ? this.props.activeOpacity! : 0);
}
this.props.onActiveStateChange?.(active);
};
render() {
const { children, style, ...rest } = this.props;
const resolvedStyle = StyleSheet.flatten(style ?? {});
return (
<BaseButton
{...rest}
style={resolvedStyle}
onActiveStateChange={this.onActiveStateChange}>
<Animated.View
style={[
btnStyles.underlay,
{
opacity: this.opacity,
backgroundColor: this.props.underlayColor,
borderRadius: resolvedStyle.borderRadius,
borderTopLeftRadius: resolvedStyle.borderTopLeftRadius,
borderTopRightRadius: resolvedStyle.borderTopRightRadius,
borderBottomLeftRadius: resolvedStyle.borderBottomLeftRadius,
borderBottomRightRadius: resolvedStyle.borderBottomRightRadius,
},
]}
/>
{children}
</BaseButton>
);
}
}
export class BorderlessButton extends React.Component<BorderlessButtonProps> {
static defaultProps = {
activeOpacity: 0.3,
borderless: true,
};
private opacity: Animated.Value;
constructor(props: BorderlessButtonProps) {
super(props);
this.opacity = new Animated.Value(1);
}
private onActiveStateChange = (active: boolean) => {
if (Platform.OS !== 'android') {
this.opacity.setValue(active ? this.props.activeOpacity! : 1);
}
this.props.onActiveStateChange?.(active);
};
render() {
const { children, style, ...rest } = this.props;
return (
<AnimatedBaseButton
{...rest}
onActiveStateChange={this.onActiveStateChange}
style={[style, Platform.OS === 'ios' && { opacity: this.opacity }]}>
{children}
</AnimatedBaseButton>
);
}
}
export { default as PureNativeButton } from './GestureHandlerButton';

View File

@@ -0,0 +1,148 @@
import * as React from 'react';
import {
PropsWithChildren,
ForwardedRef,
RefAttributes,
ReactElement,
} from 'react';
import {
ScrollView as RNScrollView,
ScrollViewProps as RNScrollViewProps,
Switch as RNSwitch,
SwitchProps as RNSwitchProps,
TextInput as RNTextInput,
TextInputProps as RNTextInputProps,
DrawerLayoutAndroid as RNDrawerLayoutAndroid,
DrawerLayoutAndroidProps as RNDrawerLayoutAndroidProps,
FlatList as RNFlatList,
FlatListProps as RNFlatListProps,
RefreshControl as RNRefreshControl,
} from 'react-native';
import createNativeWrapper from '../handlers/createNativeWrapper';
import {
NativeViewGestureHandlerProps,
nativeViewProps,
} from '../handlers/NativeViewGestureHandler';
import { toArray } from '../utils';
export const RefreshControl = createNativeWrapper(RNRefreshControl, {
disallowInterruption: true,
shouldCancelWhenOutside: false,
});
// eslint-disable-next-line @typescript-eslint/no-redeclare
export type RefreshControl = typeof RefreshControl & RNRefreshControl;
const GHScrollView = createNativeWrapper<PropsWithChildren<RNScrollViewProps>>(
RNScrollView,
{
disallowInterruption: true,
shouldCancelWhenOutside: false,
}
);
export const ScrollView = React.forwardRef<
RNScrollView,
RNScrollViewProps & NativeViewGestureHandlerProps
>((props, ref) => {
const refreshControlGestureRef = React.useRef<RefreshControl>(null);
const { refreshControl, waitFor, ...rest } = props;
return (
<GHScrollView
{...rest}
// @ts-ignore `ref` exists on `GHScrollView`
ref={ref}
waitFor={[...toArray(waitFor ?? []), refreshControlGestureRef]}
// @ts-ignore we don't pass `refreshing` prop as we only want to override the ref
refreshControl={
refreshControl
? React.cloneElement(refreshControl, {
// @ts-ignore for reasons unknown to me, `ref` doesn't exist on the type inferred by TS
ref: refreshControlGestureRef,
})
: undefined
}
/>
);
});
// backward type compatibility with https://github.com/software-mansion/react-native-gesture-handler/blob/db78d3ca7d48e8ba57482d3fe9b0a15aa79d9932/react-native-gesture-handler.d.ts#L440-L457
// include methods of wrapped components by creating an intersection type with the RN component instead of duplicating them.
// eslint-disable-next-line @typescript-eslint/no-redeclare
export type ScrollView = typeof GHScrollView & RNScrollView;
export const Switch = createNativeWrapper<RNSwitchProps>(RNSwitch, {
shouldCancelWhenOutside: false,
shouldActivateOnStart: true,
disallowInterruption: true,
});
// eslint-disable-next-line @typescript-eslint/no-redeclare
export type Switch = typeof Switch & RNSwitch;
export const TextInput = createNativeWrapper<RNTextInputProps>(RNTextInput);
// eslint-disable-next-line @typescript-eslint/no-redeclare
export type TextInput = typeof TextInput & RNTextInput;
export const DrawerLayoutAndroid = createNativeWrapper<
PropsWithChildren<RNDrawerLayoutAndroidProps>
>(RNDrawerLayoutAndroid, { disallowInterruption: true });
// eslint-disable-next-line @typescript-eslint/no-redeclare
export type DrawerLayoutAndroid = typeof DrawerLayoutAndroid &
RNDrawerLayoutAndroid;
export const FlatList = React.forwardRef((props, ref) => {
const refreshControlGestureRef = React.useRef<RefreshControl>(null);
const { waitFor, refreshControl, ...rest } = props;
const flatListProps = {};
const scrollViewProps = {};
for (const [propName, value] of Object.entries(rest)) {
// https://github.com/microsoft/TypeScript/issues/26255
if ((nativeViewProps as readonly string[]).includes(propName)) {
// @ts-ignore - this function cannot have generic type so we have to ignore this error
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
scrollViewProps[propName] = value;
} else {
// @ts-ignore - this function cannot have generic type so we have to ignore this error
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
flatListProps[propName] = value;
}
}
return (
// @ts-ignore - this function cannot have generic type so we have to ignore this error
<RNFlatList
ref={ref}
{...flatListProps}
renderScrollComponent={(scrollProps) => (
<ScrollView
{...{
...scrollProps,
...scrollViewProps,
waitFor: [...toArray(waitFor ?? []), refreshControlGestureRef],
}}
/>
)}
// @ts-ignore we don't pass `refreshing` prop as we only want to override the ref
refreshControl={
refreshControl
? React.cloneElement(refreshControl, {
// @ts-ignore for reasons unknown to me, `ref` doesn't exist on the type inferred by TS
ref: refreshControlGestureRef,
})
: undefined
}
/>
);
}) as <ItemT = any>(
props: PropsWithChildren<
RNFlatListProps<ItemT> &
RefAttributes<FlatList<ItemT>> &
NativeViewGestureHandlerProps
>,
ref: ForwardedRef<FlatList<ItemT>>
) => ReactElement | null;
// eslint-disable-next-line @typescript-eslint/no-redeclare
export type FlatList<ItemT = any> = typeof FlatList & RNFlatList<ItemT>;

View File

@@ -0,0 +1,41 @@
import * as React from 'react';
import {
FlatList as RNFlatList,
Switch as RNSwitch,
TextInput as RNTextInput,
ScrollView as RNScrollView,
FlatListProps,
View,
} from 'react-native';
import createNativeWrapper from '../handlers/createNativeWrapper';
export const ScrollView = createNativeWrapper(RNScrollView, {
disallowInterruption: false,
});
export const Switch = createNativeWrapper(RNSwitch, {
shouldCancelWhenOutside: false,
shouldActivateOnStart: true,
disallowInterruption: true,
});
export const TextInput = createNativeWrapper(RNTextInput);
export const DrawerLayoutAndroid = () => {
console.warn('DrawerLayoutAndroid is not supported on web!');
return <View />;
};
// RefreshControl is implemented as a functional component, rendering a View
// NativeViewGestureHandler needs to set a ref on its child, which cannot be done
// on functional components
export const RefreshControl = createNativeWrapper(View);
export const FlatList = React.forwardRef(
<ItemT extends any>(props: FlatListProps<ItemT>, ref: any) => (
<RNFlatList
ref={ref}
{...props}
renderScrollComponent={(scrollProps) => <ScrollView {...scrollProps} />}
/>
)
);

View File

@@ -0,0 +1,5 @@
import { HostComponent } from 'react-native';
import { RawButtonProps } from './GestureButtons';
import RNGestureHandlerButtonNativeComponent from '../specs/RNGestureHandlerButtonNativeComponent';
export default RNGestureHandlerButtonNativeComponent as HostComponent<RawButtonProps>;

View File

@@ -0,0 +1,6 @@
import * as React from 'react';
import { View } from 'react-native';
export default React.forwardRef<View>((props, ref) => (
<View ref={ref} accessibilityRole="button" {...props} />
));

View File

@@ -0,0 +1,32 @@
import * as React from 'react';
import { PropsWithChildren } from 'react';
import { ViewProps, StyleSheet } from 'react-native';
import { maybeInitializeFabric } from '../init';
import GestureHandlerRootViewContext from '../GestureHandlerRootViewContext';
import GestureHandlerRootViewNativeComponent from '../specs/RNGestureHandlerRootViewNativeComponent';
export interface GestureHandlerRootViewProps
extends PropsWithChildren<ViewProps> {}
export default function GestureHandlerRootView({
style,
...rest
}: GestureHandlerRootViewProps) {
// try initialize fabric on the first render, at this point we can
// reliably check if fabric is enabled (the function contains a flag
// to make sure it's called only once)
maybeInitializeFabric();
return (
<GestureHandlerRootViewContext.Provider value>
<GestureHandlerRootViewNativeComponent
style={style ?? styles.container}
{...rest}
/>
</GestureHandlerRootViewContext.Provider>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
});

View File

@@ -0,0 +1,28 @@
import * as React from 'react';
import { PropsWithChildren } from 'react';
import { View, ViewProps, StyleSheet } from 'react-native';
import { maybeInitializeFabric } from '../init';
import GestureHandlerRootViewContext from '../GestureHandlerRootViewContext';
export interface GestureHandlerRootViewProps
extends PropsWithChildren<ViewProps> {}
export default function GestureHandlerRootView({
style,
...rest
}: GestureHandlerRootViewProps) {
// try initialize fabric on the first render, at this point we can
// reliably check if fabric is enabled (the function contains a flag
// to make sure it's called only once)
maybeInitializeFabric();
return (
<GestureHandlerRootViewContext.Provider value>
<View style={style ?? styles.container} {...rest} />
</GestureHandlerRootViewContext.Provider>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
});

View File

@@ -0,0 +1,22 @@
import * as React from 'react';
import { PropsWithChildren } from 'react';
import { View, ViewProps, StyleSheet } from 'react-native';
import GestureHandlerRootViewContext from '../GestureHandlerRootViewContext';
export interface GestureHandlerRootViewProps
extends PropsWithChildren<ViewProps> {}
export default function GestureHandlerRootView({
style,
...rest
}: GestureHandlerRootViewProps) {
return (
<GestureHandlerRootViewContext.Provider value>
<View style={style ?? styles.container} {...rest} />
</GestureHandlerRootViewContext.Provider>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
});

View File

@@ -0,0 +1,586 @@
// Similarily to the DrawerLayout component this deserves to be put in a
// separate repo. Although, keeping it here for the time being will allow us to
// move faster and fix possible issues quicker
import * as React from 'react';
import { Component } from 'react';
import {
Animated,
StyleSheet,
View,
I18nManager,
LayoutChangeEvent,
StyleProp,
ViewStyle,
} from 'react-native';
import {
GestureEvent,
HandlerStateChangeEvent,
} from '../handlers/gestureHandlerCommon';
import {
PanGestureHandler,
PanGestureHandlerEventPayload,
PanGestureHandlerProps,
} from '../handlers/PanGestureHandler';
import {
TapGestureHandler,
TapGestureHandlerEventPayload,
} from '../handlers/TapGestureHandler';
import { State } from '../State';
const DRAG_TOSS = 0.05;
type SwipeableExcludes = Exclude<
keyof PanGestureHandlerProps,
'onGestureEvent' | 'onHandlerStateChange'
>;
// Animated.AnimatedInterpolation has been converted to a generic type
// in @types/react-native 0.70. This way we can maintain compatibility
// with all versions of @types/react-native
type AnimatedInterpolation = ReturnType<Animated.Value['interpolate']>;
export interface SwipeableProps
extends Pick<PanGestureHandlerProps, SwipeableExcludes> {
/**
* Enables two-finger gestures on supported devices, for example iPads with
* trackpads. If not enabled the gesture will require click + drag, with
* `enableTrackpadTwoFingerGesture` swiping with two fingers will also trigger
* the gesture.
*/
enableTrackpadTwoFingerGesture?: boolean;
/**
* Specifies how much the visual interaction will be delayed compared to the
* gesture distance. e.g. value of 1 will indicate that the swipeable panel
* should exactly follow the gesture, 2 means it is going to be two times
* "slower".
*/
friction?: number;
/**
* Distance from the left edge at which released panel will animate to the
* open state (or the open panel will animate into the closed state). By
* default it's a half of the panel's width.
*/
leftThreshold?: number;
/**
* Distance from the right edge at which released panel will animate to the
* open state (or the open panel will animate into the closed state). By
* default it's a half of the panel's width.
*/
rightThreshold?: number;
/**
* Distance that the panel must be dragged from the left edge to be considered
* a swipe. The default value is 10.
*/
dragOffsetFromLeftEdge?: number;
/**
* Distance that the panel must be dragged from the right edge to be considered
* a swipe. The default value is 10.
*/
dragOffsetFromRightEdge?: number;
/**
* Value indicating if the swipeable panel can be pulled further than the left
* actions panel's width. It is set to true by default as long as the left
* panel render method is present.
*/
overshootLeft?: boolean;
/**
* Value indicating if the swipeable panel can be pulled further than the
* right actions panel's width. It is set to true by default as long as the
* right panel render method is present.
*/
overshootRight?: boolean;
/**
* Specifies how much the visual interaction will be delayed compared to the
* gesture distance at overshoot. Default value is 1, it mean no friction, for
* a native feel, try 8 or above.
*/
overshootFriction?: number;
/**
* @deprecated Use `direction` argument of onSwipeableOpen()
*
* Called when left action panel gets open.
*/
onSwipeableLeftOpen?: () => void;
/**
* @deprecated Use `direction` argument of onSwipeableOpen()
*
* Called when right action panel gets open.
*/
onSwipeableRightOpen?: () => void;
/**
* Called when action panel gets open (either right or left).
*/
onSwipeableOpen?: (direction: 'left' | 'right', swipeable: Swipeable) => void;
/**
* Called when action panel is closed.
*/
onSwipeableClose?: (
direction: 'left' | 'right',
swipeable: Swipeable
) => void;
/**
* @deprecated Use `direction` argument of onSwipeableWillOpen()
*
* Called when left action panel starts animating on open.
*/
onSwipeableLeftWillOpen?: () => void;
/**
* @deprecated Use `direction` argument of onSwipeableWillOpen()
*
* Called when right action panel starts animating on open.
*/
onSwipeableRightWillOpen?: () => void;
/**
* Called when action panel starts animating on open (either right or left).
*/
onSwipeableWillOpen?: (direction: 'left' | 'right') => void;
/**
* Called when action panel starts animating on close.
*/
onSwipeableWillClose?: (direction: 'left' | 'right') => void;
/**
* Called when action panel starts being shown on dragging to open.
*/
onSwipeableOpenStartDrag?: (direction: 'left' | 'right') => void;
/**
* Called when action panel starts being shown on dragging to close.
*/
onSwipeableCloseStartDrag?: (direction: 'left' | 'right') => void;
/**
*
* This map describes the values to use as inputRange for extra interpolation:
* AnimatedValue: [startValue, endValue]
*
* progressAnimatedValue: [0, 1] dragAnimatedValue: [0, +]
*
* To support `rtl` flexbox layouts use `flexDirection` styling.
* */
renderLeftActions?: (
progressAnimatedValue: AnimatedInterpolation,
dragAnimatedValue: AnimatedInterpolation,
swipeable: Swipeable
) => React.ReactNode;
/**
*
* This map describes the values to use as inputRange for extra interpolation:
* AnimatedValue: [startValue, endValue]
*
* progressAnimatedValue: [0, 1] dragAnimatedValue: [0, -]
*
* To support `rtl` flexbox layouts use `flexDirection` styling.
* */
renderRightActions?: (
progressAnimatedValue: AnimatedInterpolation,
dragAnimatedValue: AnimatedInterpolation,
swipeable: Swipeable
) => React.ReactNode;
useNativeAnimations?: boolean;
animationOptions?: Record<string, unknown>;
/**
* Style object for the container (`Animated.View`), for example to override
* `overflow: 'hidden'`.
*/
containerStyle?: StyleProp<ViewStyle>;
/**
* Style object for the children container (`Animated.View`), for example to
* apply `flex: 1`
*/
childrenContainerStyle?: StyleProp<ViewStyle>;
}
type SwipeableState = {
dragX: Animated.Value;
rowTranslation: Animated.Value;
rowState: number;
leftWidth?: number;
rightOffset?: number;
rowWidth?: number;
};
export default class Swipeable extends Component<
SwipeableProps,
SwipeableState
> {
static defaultProps = {
friction: 1,
overshootFriction: 1,
useNativeAnimations: true,
};
constructor(props: SwipeableProps) {
super(props);
const dragX = new Animated.Value(0);
this.state = {
dragX,
rowTranslation: new Animated.Value(0),
rowState: 0,
leftWidth: undefined,
rightOffset: undefined,
rowWidth: undefined,
};
this.updateAnimatedEvent(props, this.state);
this.onGestureEvent = Animated.event(
[{ nativeEvent: { translationX: dragX } }],
{ useNativeDriver: props.useNativeAnimations! }
);
}
shouldComponentUpdate(props: SwipeableProps, state: SwipeableState) {
if (
this.props.friction !== props.friction ||
this.props.overshootLeft !== props.overshootLeft ||
this.props.overshootRight !== props.overshootRight ||
this.props.overshootFriction !== props.overshootFriction ||
this.state.leftWidth !== state.leftWidth ||
this.state.rightOffset !== state.rightOffset ||
this.state.rowWidth !== state.rowWidth
) {
this.updateAnimatedEvent(props, state);
}
return true;
}
private onGestureEvent?: (
event: GestureEvent<PanGestureHandlerEventPayload>
) => void;
private transX?: AnimatedInterpolation;
private showLeftAction?: AnimatedInterpolation | Animated.Value;
private leftActionTranslate?: AnimatedInterpolation;
private showRightAction?: AnimatedInterpolation | Animated.Value;
private rightActionTranslate?: AnimatedInterpolation;
private updateAnimatedEvent = (
props: SwipeableProps,
state: SwipeableState
) => {
const { friction, overshootFriction } = props;
const { dragX, rowTranslation, leftWidth = 0, rowWidth = 0 } = state;
const { rightOffset = rowWidth } = state;
const rightWidth = Math.max(0, rowWidth - rightOffset);
const { overshootLeft = leftWidth > 0, overshootRight = rightWidth > 0 } =
props;
const transX = Animated.add(
rowTranslation,
dragX.interpolate({
inputRange: [0, friction!],
outputRange: [0, 1],
})
).interpolate({
inputRange: [-rightWidth - 1, -rightWidth, leftWidth, leftWidth + 1],
outputRange: [
-rightWidth - (overshootRight ? 1 / overshootFriction! : 0),
-rightWidth,
leftWidth,
leftWidth + (overshootLeft ? 1 / overshootFriction! : 0),
],
});
this.transX = transX;
this.showLeftAction =
leftWidth > 0
? transX.interpolate({
inputRange: [-1, 0, leftWidth],
outputRange: [0, 0, 1],
})
: new Animated.Value(0);
this.leftActionTranslate = this.showLeftAction.interpolate({
inputRange: [0, Number.MIN_VALUE],
outputRange: [-10000, 0],
extrapolate: 'clamp',
});
this.showRightAction =
rightWidth > 0
? transX.interpolate({
inputRange: [-rightWidth, 0, 1],
outputRange: [1, 0, 0],
})
: new Animated.Value(0);
this.rightActionTranslate = this.showRightAction.interpolate({
inputRange: [0, Number.MIN_VALUE],
outputRange: [-10000, 0],
extrapolate: 'clamp',
});
};
private onTapHandlerStateChange = ({
nativeEvent,
}: HandlerStateChangeEvent<TapGestureHandlerEventPayload>) => {
if (nativeEvent.oldState === State.ACTIVE) {
this.close();
}
};
private onHandlerStateChange = (
ev: HandlerStateChangeEvent<PanGestureHandlerEventPayload>
) => {
if (ev.nativeEvent.oldState === State.ACTIVE) {
this.handleRelease(ev);
}
if (ev.nativeEvent.state === State.ACTIVE) {
const { velocityX, translationX: dragX } = ev.nativeEvent;
const { rowState } = this.state;
const { friction } = this.props;
const translationX = (dragX + DRAG_TOSS * velocityX) / friction!;
const direction =
rowState === -1
? 'right'
: rowState === 1
? 'left'
: translationX > 0
? 'left'
: 'right';
if (rowState === 0) {
this.props.onSwipeableOpenStartDrag?.(direction);
} else {
this.props.onSwipeableCloseStartDrag?.(direction);
}
}
};
private handleRelease = (
ev: HandlerStateChangeEvent<PanGestureHandlerEventPayload>
) => {
const { velocityX, translationX: dragX } = ev.nativeEvent;
const { leftWidth = 0, rowWidth = 0, rowState } = this.state;
const { rightOffset = rowWidth } = this.state;
const rightWidth = rowWidth - rightOffset;
const {
friction,
leftThreshold = leftWidth / 2,
rightThreshold = rightWidth / 2,
} = this.props;
const startOffsetX = this.currentOffset() + dragX / friction!;
const translationX = (dragX + DRAG_TOSS * velocityX) / friction!;
let toValue = 0;
if (rowState === 0) {
if (translationX > leftThreshold) {
toValue = leftWidth;
} else if (translationX < -rightThreshold) {
toValue = -rightWidth;
}
} else if (rowState === 1) {
// swiped to left
if (translationX > -leftThreshold) {
toValue = leftWidth;
}
} else {
// swiped to right
if (translationX < rightThreshold) {
toValue = -rightWidth;
}
}
this.animateRow(startOffsetX, toValue, velocityX / friction!);
};
private animateRow = (
fromValue: number,
toValue: number,
velocityX?:
| number
| {
x: number;
y: number;
}
) => {
const { dragX, rowTranslation } = this.state;
dragX.setValue(0);
rowTranslation.setValue(fromValue);
this.setState({ rowState: Math.sign(toValue) });
Animated.spring(rowTranslation, {
restSpeedThreshold: 1.7,
restDisplacementThreshold: 0.4,
velocity: velocityX,
bounciness: 0,
toValue,
useNativeDriver: this.props.useNativeAnimations!,
...this.props.animationOptions,
}).start(({ finished }) => {
if (finished) {
if (toValue > 0) {
this.props.onSwipeableLeftOpen?.();
this.props.onSwipeableOpen?.('left', this);
} else if (toValue < 0) {
this.props.onSwipeableRightOpen?.();
this.props.onSwipeableOpen?.('right', this);
} else {
const closingDirection = fromValue > 0 ? 'left' : 'right';
this.props.onSwipeableClose?.(closingDirection, this);
}
}
});
if (toValue > 0) {
this.props.onSwipeableLeftWillOpen?.();
this.props.onSwipeableWillOpen?.('left');
} else if (toValue < 0) {
this.props.onSwipeableRightWillOpen?.();
this.props.onSwipeableWillOpen?.('right');
} else {
const closingDirection = fromValue > 0 ? 'left' : 'right';
this.props.onSwipeableWillClose?.(closingDirection);
}
};
private onRowLayout = ({ nativeEvent }: LayoutChangeEvent) => {
this.setState({ rowWidth: nativeEvent.layout.width });
};
private currentOffset = () => {
const { leftWidth = 0, rowWidth = 0, rowState } = this.state;
const { rightOffset = rowWidth } = this.state;
const rightWidth = rowWidth - rightOffset;
if (rowState === 1) {
return leftWidth;
} else if (rowState === -1) {
return -rightWidth;
}
return 0;
};
close = () => {
this.animateRow(this.currentOffset(), 0);
};
openLeft = () => {
const { leftWidth = 0 } = this.state;
this.animateRow(this.currentOffset(), leftWidth);
};
openRight = () => {
const { rowWidth = 0 } = this.state;
const { rightOffset = rowWidth } = this.state;
const rightWidth = rowWidth - rightOffset;
this.animateRow(this.currentOffset(), -rightWidth);
};
reset = () => {
const { dragX, rowTranslation } = this.state;
dragX.setValue(0);
rowTranslation.setValue(0);
this.setState({ rowState: 0 });
};
render() {
const { rowState } = this.state;
const {
children,
renderLeftActions,
renderRightActions,
dragOffsetFromLeftEdge = 10,
dragOffsetFromRightEdge = 10,
} = this.props;
const left = renderLeftActions && (
<Animated.View
style={[
styles.leftActions,
// all those and below parameters can have ! since they are all
// asigned in constructor in `updateAnimatedEvent` but TS cannot spot
// it for some reason
{ transform: [{ translateX: this.leftActionTranslate! }] },
]}>
{renderLeftActions(this.showLeftAction!, this.transX!, this)}
<View
onLayout={({ nativeEvent }) =>
this.setState({ leftWidth: nativeEvent.layout.x })
}
/>
</Animated.View>
);
const right = renderRightActions && (
<Animated.View
style={[
styles.rightActions,
{ transform: [{ translateX: this.rightActionTranslate! }] },
]}>
{renderRightActions(this.showRightAction!, this.transX!, this)}
<View
onLayout={({ nativeEvent }) =>
this.setState({ rightOffset: nativeEvent.layout.x })
}
/>
</Animated.View>
);
return (
<PanGestureHandler
activeOffsetX={[-dragOffsetFromRightEdge, dragOffsetFromLeftEdge]}
touchAction="pan-y"
{...this.props}
onGestureEvent={this.onGestureEvent}
onHandlerStateChange={this.onHandlerStateChange}>
<Animated.View
onLayout={this.onRowLayout}
style={[styles.container, this.props.containerStyle]}>
{left}
{right}
<TapGestureHandler
enabled={rowState !== 0}
touchAction="pan-y"
onHandlerStateChange={this.onTapHandlerStateChange}>
<Animated.View
pointerEvents={rowState === 0 ? 'auto' : 'box-only'}
style={[
{
transform: [{ translateX: this.transX! }],
},
this.props.childrenContainerStyle,
]}>
{children}
</Animated.View>
</TapGestureHandler>
</Animated.View>
</PanGestureHandler>
);
}
}
const styles = StyleSheet.create({
container: {
overflow: 'hidden',
},
leftActions: {
...StyleSheet.absoluteFillObject,
flexDirection: I18nManager.isRTL ? 'row-reverse' : 'row',
},
rightActions: {
...StyleSheet.absoluteFillObject,
flexDirection: I18nManager.isRTL ? 'row' : 'row-reverse',
},
});

View File

@@ -0,0 +1,30 @@
import * as React from 'react';
import { StyleSheet, StyleProp, ViewStyle } from 'react-native';
import hoistNonReactStatics from 'hoist-non-react-statics';
import GestureHandlerRootView from './GestureHandlerRootView';
export default function gestureHandlerRootHOC<P extends object>(
Component: React.ComponentType<P>,
containerStyles?: StyleProp<ViewStyle>
): React.ComponentType<P> {
function Wrapper(props: P) {
return (
<GestureHandlerRootView style={[styles.container, containerStyles]}>
<Component {...props} />
</GestureHandlerRootView>
);
}
Wrapper.displayName = `gestureHandlerRootHOC(${
Component.displayName || Component.name
})`;
// @ts-ignore - hoistNonReactStatics uses old version of @types/react
hoistNonReactStatics(Wrapper, Component);
return Wrapper;
}
const styles = StyleSheet.create({
container: { flex: 1 },
});

View File

@@ -0,0 +1,304 @@
import * as React from 'react';
import { Component } from 'react';
import {
Animated,
Platform,
StyleProp,
ViewStyle,
TouchableWithoutFeedbackProps,
Insets,
} from 'react-native';
import { State } from '../../State';
import { BaseButton } from '../GestureButtons';
import {
GestureEvent,
HandlerStateChangeEvent,
UserSelect,
} from '../../handlers/gestureHandlerCommon';
import { NativeViewGestureHandlerPayload } from '../../handlers/NativeViewGestureHandler';
import { TouchableNativeFeedbackExtraProps } from './TouchableNativeFeedback.android';
/**
* Each touchable is a states' machine which preforms transitions.
* On very beginning (and on the very end or recognition) touchable is
* UNDETERMINED. Then it moves to BEGAN. If touchable recognizes that finger
* travel outside it transits to special MOVED_OUTSIDE state. Gesture recognition
* finishes in UNDETERMINED state.
*/
export const TOUCHABLE_STATE = {
UNDETERMINED: 0,
BEGAN: 1,
MOVED_OUTSIDE: 2,
} as const;
type TouchableState = typeof TOUCHABLE_STATE[keyof typeof TOUCHABLE_STATE];
export interface GenericTouchableProps
extends Omit<TouchableWithoutFeedbackProps, 'hitSlop'> {
// Decided to drop not used fields from RN's implementation.
// e.g. onBlur and onFocus as well as deprecated props. - TODO: this comment may be unuseful in this moment
// TODO: in RN these events get native event parameter, which prolly could be used in our implementation too
onPress?: () => void;
onPressIn?: () => void;
onPressOut?: () => void;
onLongPress?: () => void;
nativeID?: string;
shouldActivateOnStart?: boolean;
disallowInterruption?: boolean;
containerStyle?: StyleProp<ViewStyle>;
hitSlop?: Insets | number;
userSelect?: UserSelect;
}
interface InternalProps {
extraButtonProps: TouchableNativeFeedbackExtraProps;
onStateChange?: (oldState: TouchableState, newState: TouchableState) => void;
}
// TODO: maybe can be better
// TODO: all clearTimeout have ! added, maybe they shouldn't ?
type Timeout = ReturnType<typeof setTimeout> | null | undefined;
/**
* GenericTouchable is not intented to be used as it is.
* Should be treated as a source for the rest of touchables
*/
export default class GenericTouchable extends Component<
GenericTouchableProps & InternalProps
> {
static defaultProps = {
delayLongPress: 600,
extraButtonProps: {
rippleColor: 'transparent',
exclusive: true,
},
};
// timeout handlers
pressInTimeout: Timeout;
pressOutTimeout: Timeout;
longPressTimeout: Timeout;
// This flag is required since recognition of longPress implies not-invoking onPress
longPressDetected = false;
pointerInside = true;
// State of touchable
STATE: TouchableState = TOUCHABLE_STATE.UNDETERMINED;
// handlePressIn in called on first touch on traveling inside component.
// Handles state transition with delay.
handlePressIn() {
if (this.props.delayPressIn) {
this.pressInTimeout = setTimeout(() => {
this.moveToState(TOUCHABLE_STATE.BEGAN);
this.pressInTimeout = null;
}, this.props.delayPressIn);
} else {
this.moveToState(TOUCHABLE_STATE.BEGAN);
}
if (this.props.onLongPress) {
const time =
(this.props.delayPressIn || 0) + (this.props.delayLongPress || 0);
this.longPressTimeout = setTimeout(this.onLongPressDetected, time);
}
}
// handleMoveOutside in called on traveling outside component.
// Handles state transition with delay.
handleMoveOutside() {
if (this.props.delayPressOut) {
this.pressOutTimeout =
this.pressOutTimeout ||
setTimeout(() => {
this.moveToState(TOUCHABLE_STATE.MOVED_OUTSIDE);
this.pressOutTimeout = null;
}, this.props.delayPressOut);
} else {
this.moveToState(TOUCHABLE_STATE.MOVED_OUTSIDE);
}
}
// handleGoToUndetermined transits to UNDETERMINED state with proper delay
handleGoToUndetermined() {
clearTimeout(this.pressOutTimeout!); // TODO: maybe it can be undefined
if (this.props.delayPressOut) {
this.pressOutTimeout = setTimeout(() => {
if (this.STATE === TOUCHABLE_STATE.UNDETERMINED) {
this.moveToState(TOUCHABLE_STATE.BEGAN);
}
this.moveToState(TOUCHABLE_STATE.UNDETERMINED);
this.pressOutTimeout = null;
}, this.props.delayPressOut);
} else {
if (this.STATE === TOUCHABLE_STATE.UNDETERMINED) {
this.moveToState(TOUCHABLE_STATE.BEGAN);
}
this.moveToState(TOUCHABLE_STATE.UNDETERMINED);
}
}
componentDidMount() {
this.reset();
}
// reset timeout to prevent memory leaks.
reset() {
this.longPressDetected = false;
this.pointerInside = true;
clearTimeout(this.pressInTimeout!);
clearTimeout(this.pressOutTimeout!);
clearTimeout(this.longPressTimeout!);
this.pressOutTimeout = null;
this.longPressTimeout = null;
this.pressInTimeout = null;
}
// All states' transitions are defined here.
moveToState(newState: TouchableState) {
if (newState === this.STATE) {
// Ignore dummy transitions
return;
}
if (newState === TOUCHABLE_STATE.BEGAN) {
// First touch and moving inside
this.props.onPressIn?.();
} else if (newState === TOUCHABLE_STATE.MOVED_OUTSIDE) {
// Moving outside
this.props.onPressOut?.();
} else if (newState === TOUCHABLE_STATE.UNDETERMINED) {
// Need to reset each time on transition to UNDETERMINED
this.reset();
if (this.STATE === TOUCHABLE_STATE.BEGAN) {
// ... and if it happens inside button.
this.props.onPressOut?.();
}
}
// Finally call lister (used by subclasses)
this.props.onStateChange?.(this.STATE, newState);
// ... and make transition.
this.STATE = newState;
}
onGestureEvent = ({
nativeEvent: { pointerInside },
}: GestureEvent<NativeViewGestureHandlerPayload>) => {
if (this.pointerInside !== pointerInside) {
if (pointerInside) {
this.onMoveIn();
} else {
this.onMoveOut();
}
}
this.pointerInside = pointerInside;
};
onHandlerStateChange = ({
nativeEvent,
}: HandlerStateChangeEvent<NativeViewGestureHandlerPayload>) => {
const { state } = nativeEvent;
if (state === State.CANCELLED || state === State.FAILED) {
// Need to handle case with external cancellation (e.g. by ScrollView)
this.moveToState(TOUCHABLE_STATE.UNDETERMINED);
} else if (
// This platform check is an implication of slightly different behavior of handlers on different platform.
// And Android "Active" state is achieving on first move of a finger, not on press in.
// On iOS event on "Began" is not delivered.
state === (Platform.OS !== 'android' ? State.ACTIVE : State.BEGAN) &&
this.STATE === TOUCHABLE_STATE.UNDETERMINED
) {
// Moving inside requires
this.handlePressIn();
} else if (state === State.END) {
const shouldCallOnPress =
!this.longPressDetected &&
this.STATE !== TOUCHABLE_STATE.MOVED_OUTSIDE &&
this.pressOutTimeout === null;
this.handleGoToUndetermined();
if (shouldCallOnPress) {
// Calls only inside component whether no long press was called previously
this.props.onPress?.();
}
}
};
onLongPressDetected = () => {
this.longPressDetected = true;
// checked for in the caller of `onLongPressDetected`, but better to check twice
this.props.onLongPress?.();
};
componentWillUnmount() {
// to prevent memory leaks
this.reset();
}
onMoveIn() {
if (this.STATE === TOUCHABLE_STATE.MOVED_OUTSIDE) {
// This call is not throttled with delays (like in RN's implementation).
this.moveToState(TOUCHABLE_STATE.BEGAN);
}
}
onMoveOut() {
// long press should no longer be detected
clearTimeout(this.longPressTimeout!);
this.longPressTimeout = null;
if (this.STATE === TOUCHABLE_STATE.BEGAN) {
this.handleMoveOutside();
}
}
render() {
const hitSlop =
(typeof this.props.hitSlop === 'number'
? {
top: this.props.hitSlop,
left: this.props.hitSlop,
bottom: this.props.hitSlop,
right: this.props.hitSlop,
}
: this.props.hitSlop) ?? undefined;
const coreProps = {
accessible: this.props.accessible !== false,
accessibilityLabel: this.props.accessibilityLabel,
accessibilityHint: this.props.accessibilityHint,
accessibilityRole: this.props.accessibilityRole,
// TODO: check if changed to no 's' correctly, also removed 2 props that are no longer available: `accessibilityComponentType` and `accessibilityTraits`,
// would be good to check if it is ok for sure, see: https://github.com/facebook/react-native/issues/24016
accessibilityState: this.props.accessibilityState,
accessibilityActions: this.props.accessibilityActions,
onAccessibilityAction: this.props.onAccessibilityAction,
nativeID: this.props.nativeID,
onLayout: this.props.onLayout,
};
return (
<BaseButton
style={this.props.containerStyle}
onHandlerStateChange={
// TODO: not sure if it can be undefined instead of null
this.props.disabled ? undefined : this.onHandlerStateChange
}
onGestureEvent={this.onGestureEvent}
hitSlop={hitSlop}
userSelect={this.props.userSelect}
shouldActivateOnStart={this.props.shouldActivateOnStart}
disallowInterruption={this.props.disallowInterruption}
testID={this.props.testID}
touchSoundDisabled={this.props.touchSoundDisabled ?? false}
enabled={!this.props.disabled}
{...this.props.extraButtonProps}>
<Animated.View {...coreProps} style={this.props.style}>
{this.props.children}
</Animated.View>
</BaseButton>
);
}
}

View File

@@ -0,0 +1,115 @@
import * as React from 'react';
import { Component } from 'react';
import GenericTouchable, {
GenericTouchableProps,
TOUCHABLE_STATE,
} from './GenericTouchable';
import {
StyleSheet,
View,
TouchableHighlightProps as RNTouchableHighlightProps,
ColorValue,
ViewProps,
} from 'react-native';
interface State {
extraChildStyle: null | {
opacity?: number;
};
extraUnderlayStyle: null | {
backgroundColor?: ColorValue;
};
}
export type TouchableHighlightProps = RNTouchableHighlightProps &
GenericTouchableProps;
/**
* TouchableHighlight follows RN's implementation
*/
export default class TouchableHighlight extends Component<
TouchableHighlightProps,
State
> {
static defaultProps = {
...GenericTouchable.defaultProps,
activeOpacity: 0.85,
delayPressOut: 100,
underlayColor: 'black',
};
constructor(props: TouchableHighlightProps) {
super(props);
this.state = {
extraChildStyle: null,
extraUnderlayStyle: null,
};
}
// Copied from RN
showUnderlay = () => {
if (!this.hasPressHandler()) {
return;
}
this.setState({
extraChildStyle: {
opacity: this.props.activeOpacity,
},
extraUnderlayStyle: {
backgroundColor: this.props.underlayColor,
},
});
this.props.onShowUnderlay?.();
};
hasPressHandler = () =>
this.props.onPress ||
this.props.onPressIn ||
this.props.onPressOut ||
this.props.onLongPress;
hideUnderlay = () => {
this.setState({
extraChildStyle: null,
extraUnderlayStyle: null,
});
this.props.onHideUnderlay?.();
};
renderChildren() {
if (!this.props.children) {
return <View />;
}
const child = React.Children.only(
this.props.children
) as React.ReactElement<ViewProps>; // TODO: not sure if OK but fixes error
return React.cloneElement(child, {
style: StyleSheet.compose(child.props.style, this.state.extraChildStyle),
});
}
onStateChange = (_from: number, to: number) => {
if (to === TOUCHABLE_STATE.BEGAN) {
this.showUnderlay();
} else if (
to === TOUCHABLE_STATE.UNDETERMINED ||
to === TOUCHABLE_STATE.MOVED_OUTSIDE
) {
this.hideUnderlay();
}
};
render() {
const { style = {}, ...rest } = this.props;
const { extraUnderlayStyle } = this.state;
return (
<GenericTouchable
{...rest}
style={[style, extraUnderlayStyle]}
onStateChange={this.onStateChange}>
{this.renderChildren()}
</GenericTouchable>
);
}
}

View File

@@ -0,0 +1,91 @@
import {
Platform,
TouchableNativeFeedbackProps as RNTouchableNativeFeedbackProps,
ColorValue,
} from 'react-native';
import * as React from 'react';
import { Component } from 'react';
import GenericTouchable, { GenericTouchableProps } from './GenericTouchable';
export type TouchableNativeFeedbackExtraProps = {
borderless?: boolean;
rippleColor?: number | null;
rippleRadius?: number | null;
foreground?: boolean;
};
export type TouchableNativeFeedbackProps = RNTouchableNativeFeedbackProps &
GenericTouchableProps;
/**
* TouchableNativeFeedback behaves slightly different than RN's TouchableNativeFeedback.
* There's small difference with handling long press ripple since RN's implementation calls
* ripple animation via bridge. This solution leaves all animations' handling for native components so
* it follows native behaviours.
*/
export default class TouchableNativeFeedback extends Component<TouchableNativeFeedbackProps> {
static defaultProps = {
...GenericTouchable.defaultProps,
useForeground: true,
extraButtonProps: {
// Disable hiding ripple on Android
rippleColor: null,
},
};
// could be taken as RNTouchableNativeFeedback.SelectableBackground etc. but the API may change
static SelectableBackground = (rippleRadius?: number) => ({
type: 'ThemeAttrAndroid',
// I added `attribute` prop to clone the implementation of RN and be able to use only 2 types
attribute: 'selectableItemBackground',
rippleRadius,
});
static SelectableBackgroundBorderless = (rippleRadius?: number) => ({
type: 'ThemeAttrAndroid',
attribute: 'selectableItemBackgroundBorderless',
rippleRadius,
});
static Ripple = (
color: ColorValue,
borderless: boolean,
rippleRadius?: number
) => ({
type: 'RippleAndroid',
color,
borderless,
rippleRadius,
});
static canUseNativeForeground = () =>
Platform.OS === 'android' && Platform.Version >= 23;
getExtraButtonProps() {
const extraProps: TouchableNativeFeedbackExtraProps = {};
const { background } = this.props;
if (background) {
// I changed type values to match those used in RN
// TODO(TS): check if it works the same as previous implementation - looks like it works the same as RN component, so it should be ok
if (background.type === 'RippleAndroid') {
extraProps['borderless'] = background.borderless;
extraProps['rippleColor'] = background.color;
} else if (background.type === 'ThemeAttrAndroid') {
extraProps['borderless'] =
background.attribute === 'selectableItemBackgroundBorderless';
}
// I moved it from above since it should be available in all options
extraProps['rippleRadius'] = background.rippleRadius;
}
extraProps['foreground'] = this.props.useForeground;
return extraProps;
}
render() {
const { style = {}, ...rest } = this.props;
return (
<GenericTouchable
{...rest}
style={style}
extraButtonProps={this.getExtraButtonProps()}
/>
);
}
}

View File

@@ -0,0 +1,3 @@
import { TouchableNativeFeedback } from 'react-native';
export default TouchableNativeFeedback;

View File

@@ -0,0 +1,75 @@
import {
Animated,
Easing,
StyleSheet,
View,
TouchableOpacityProps as RNTouchableOpacityProps,
} from 'react-native';
import GenericTouchable, {
TOUCHABLE_STATE,
GenericTouchableProps,
} from './GenericTouchable';
import * as React from 'react';
import { Component } from 'react';
export type TouchableOpacityProps = RNTouchableOpacityProps &
GenericTouchableProps & {
useNativeAnimations?: boolean;
};
/**
* TouchableOpacity bases on timing animation which has been used in RN's core
*/
export default class TouchableOpacity extends Component<TouchableOpacityProps> {
static defaultProps = {
...GenericTouchable.defaultProps,
activeOpacity: 0.2,
};
// opacity is 1 one by default but could be overwritten
getChildStyleOpacityWithDefault = () => {
const childStyle = StyleSheet.flatten(this.props.style) || {};
return childStyle.opacity == null
? 1
: (childStyle.opacity.valueOf() as number);
};
opacity = new Animated.Value(this.getChildStyleOpacityWithDefault());
setOpacityTo = (value: number, duration: number) => {
Animated.timing(this.opacity, {
toValue: value,
duration: duration,
easing: Easing.inOut(Easing.quad),
useNativeDriver: this.props.useNativeAnimations ?? true,
}).start();
};
onStateChange = (_from: number, to: number) => {
if (to === TOUCHABLE_STATE.BEGAN) {
this.setOpacityTo(this.props.activeOpacity!, 0);
} else if (
to === TOUCHABLE_STATE.UNDETERMINED ||
to === TOUCHABLE_STATE.MOVED_OUTSIDE
) {
this.setOpacityTo(this.getChildStyleOpacityWithDefault(), 150);
}
};
render() {
const { style = {}, ...rest } = this.props;
return (
<GenericTouchable
{...rest}
style={[
style,
{
opacity: this.opacity as unknown as number, // TODO: fix this
},
]}
onStateChange={this.onStateChange}>
{this.props.children ? this.props.children : <View />}
</GenericTouchable>
);
}
}

View File

@@ -0,0 +1,14 @@
import * as React from 'react';
import { PropsWithChildren } from 'react';
import GenericTouchable, { GenericTouchableProps } from './GenericTouchable';
export type TouchableWithoutFeedbackProps = GenericTouchableProps;
const TouchableWithoutFeedback = React.forwardRef<
GenericTouchable,
PropsWithChildren<TouchableWithoutFeedbackProps>
>((props, ref) => <GenericTouchable ref={ref} {...props} />);
TouchableWithoutFeedback.defaultProps = GenericTouchable.defaultProps;
export default TouchableWithoutFeedback;

View File

@@ -0,0 +1,7 @@
export type { TouchableHighlightProps } from './TouchableHighlight';
export type { TouchableOpacityProps } from './TouchableOpacity';
export type { TouchableWithoutFeedbackProps } from './TouchableWithoutFeedback';
export { default as TouchableNativeFeedback } from './TouchableNativeFeedback';
export { default as TouchableWithoutFeedback } from './TouchableWithoutFeedback';
export { default as TouchableOpacity } from './TouchableOpacity';
export { default as TouchableHighlight } from './TouchableHighlight';

View File

@@ -0,0 +1,11 @@
import pack from 'react-native/package.json';
const [majorStr, minorStr] = pack.version.split('.');
const REACT_NATIVE_VERSION = {
major: parseInt(majorStr, 10),
minor: parseInt(minorStr, 10),
};
export function getReactNativeVersion() {
return REACT_NATIVE_VERSION;
}

View File

@@ -0,0 +1,3 @@
export function getReactNativeVersion() {
throw new Error('getReactNativeVersion is not supported on web');
}

View File

@@ -0,0 +1,44 @@
// Used by GestureDetector (unsupported on web at the moment) to check whether the
// attached view may get flattened on Fabric. This implementation causes errors
// on web due to the static resolution of `require` statements by webpack breaking
// the conditional importing. Solved by making .web file.
let findHostInstance_DEPRECATED: (ref: unknown) => void;
let getInternalInstanceHandleFromPublicInstance: (ref: unknown) => {
stateNode: { node: unknown };
};
export function getShadowNodeFromRef(ref: unknown) {
// load findHostInstance_DEPRECATED lazily because it may not be available before render
if (findHostInstance_DEPRECATED === undefined) {
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
findHostInstance_DEPRECATED =
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-member-access
require('react-native/Libraries/Renderer/shims/ReactFabric').findHostInstance_DEPRECATED;
} catch (e) {
findHostInstance_DEPRECATED = (_ref: unknown) => null;
}
}
// load findHostInstance_DEPRECATED lazily because it may not be available before render
if (getInternalInstanceHandleFromPublicInstance === undefined) {
try {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
getInternalInstanceHandleFromPublicInstance =
// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-member-access
require('react-native/Libraries/ReactNative/ReactFabricPublicInstance/ReactFabricPublicInstance')
.getInternalInstanceHandleFromPublicInstance ??
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return
((ref: any) => ref._internalInstanceHandle);
} catch (e) {
getInternalInstanceHandleFromPublicInstance = (ref: any) =>
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-return
ref._internalInstanceHandle;
}
}
// @ts-ignore Fabric
return getInternalInstanceHandleFromPublicInstance(
findHostInstance_DEPRECATED(ref)
).stateNode.node;
}

View File

@@ -0,0 +1,7 @@
// Used by GestureDetector (unsupported on web at the moment) to check whether the
// attached view may get flattened on Fabric. Original implementation causes errors
// on web due to the static resolution of `require` statements by webpack breaking
// the conditional importing.
export function getShadowNodeFromRef(_ref: any) {
return null;
}

View File

@@ -0,0 +1,5 @@
// `queueMicrotask` was introduced to react-native in version 0.66 (https://github.com/react-native-community/releases/blob/master/CHANGELOG.md#v0660)
// Because Gesture Handler supports versions 0.64+, we have to handle situations where someone uses older version of react native.
// That's why if `queueMicrotask` doesn't exist, we use `setImmediate` instead, since it was used before we switched to `queueMicrotask` in version 2.11.0
export const ghQueueMicrotask =
typeof queueMicrotask === 'function' ? queueMicrotask : setImmediate;

View File

@@ -0,0 +1,59 @@
import createHandler from './createHandler';
import {
BaseGestureHandlerProps,
baseGestureHandlerProps,
} from './gestureHandlerCommon';
export const flingGestureHandlerProps = [
'numberOfPointers',
'direction',
] as const;
export type FlingGestureHandlerEventPayload = {
x: number;
y: number;
absoluteX: number;
absoluteY: number;
};
export interface FlingGestureConfig {
/**
* Expressed allowed direction of movement. It's possible to pass one or many
* directions in one parameter:
*
* ```js
* direction={Directions.RIGHT | Directions.LEFT}
* ```
*
* or
*
* ```js
* direction={Directions.DOWN}
* ```
*/
direction?: number;
/**
* Determine exact number of points required to handle the fling gesture.
*/
numberOfPointers?: number;
}
export interface FlingGestureHandlerProps
extends BaseGestureHandlerProps<FlingGestureHandlerEventPayload>,
FlingGestureConfig {}
export const flingHandlerName = 'FlingGestureHandler';
export type FlingGestureHandler = typeof FlingGestureHandler;
// eslint-disable-next-line @typescript-eslint/no-redeclare -- backward compatibility; see description on the top of gestureHandlerCommon.ts file
export const FlingGestureHandler = createHandler<
FlingGestureHandlerProps,
FlingGestureHandlerEventPayload
>({
name: flingHandlerName,
allowedProps: [
...baseGestureHandlerProps,
...flingGestureHandlerProps,
] as const,
config: {},
});

View File

@@ -0,0 +1,90 @@
import React, { PropsWithChildren } from 'react';
import { tagMessage } from '../utils';
import PlatformConstants from '../PlatformConstants';
import createHandler from './createHandler';
import {
BaseGestureHandlerProps,
baseGestureHandlerProps,
} from './gestureHandlerCommon';
export const forceTouchGestureHandlerProps = [
'minForce',
'maxForce',
'feedbackOnActivation',
] as const;
// implicit `children` prop has been removed in @types/react^18.0.0
class ForceTouchFallback extends React.Component<PropsWithChildren<unknown>> {
static forceTouchAvailable = false;
componentDidMount() {
console.warn(
tagMessage(
'ForceTouchGestureHandler is not available on this platform. Please use ForceTouchGestureHandler.forceTouchAvailable to conditionally render other components that would provide a fallback behavior specific to your usecase'
)
);
}
render() {
return this.props.children;
}
}
export type ForceTouchGestureHandlerEventPayload = {
x: number;
y: number;
absoluteX: number;
absoluteY: number;
/**
* The pressure of a touch.
*/
force: number;
};
export interface ForceTouchGestureConfig {
/**
*
* A minimal pressure that is required before handler can activate. Should be a
* value from range `[0.0, 1.0]`. Default is `0.2`.
*/
minForce?: number;
/**
* A maximal pressure that could be applied for handler. If the pressure is
* greater, handler fails. Should be a value from range `[0.0, 1.0]`.
*/
maxForce?: number;
/**
* Boolean value defining if haptic feedback has to be performed on
* activation.
*/
feedbackOnActivation?: boolean;
}
export interface ForceTouchGestureHandlerProps
extends BaseGestureHandlerProps<ForceTouchGestureHandlerEventPayload>,
ForceTouchGestureConfig {}
export type ForceTouchGestureHandler = typeof ForceTouchGestureHandler & {
forceTouchAvailable: boolean;
};
export const forceTouchHandlerName = 'ForceTouchGestureHandler';
// eslint-disable-next-line @typescript-eslint/no-redeclare -- backward compatibility; see description on the top of gestureHandlerCommon.ts file
export const ForceTouchGestureHandler = PlatformConstants?.forceTouchAvailable
? createHandler<
ForceTouchGestureHandlerProps,
ForceTouchGestureHandlerEventPayload
>({
name: forceTouchHandlerName,
allowedProps: [
...baseGestureHandlerProps,
...forceTouchGestureHandlerProps,
] as const,
config: {},
})
: ForceTouchFallback;
(ForceTouchGestureHandler as ForceTouchGestureHandler).forceTouchAvailable =
PlatformConstants?.forceTouchAvailable || false;

View File

@@ -0,0 +1,88 @@
import createHandler from './createHandler';
import {
BaseGestureHandlerProps,
baseGestureHandlerProps,
} from './gestureHandlerCommon';
export const longPressGestureHandlerProps = [
'minDurationMs',
'maxDist',
] as const;
export type LongPressGestureHandlerEventPayload = {
/**
* X coordinate, expressed in points, of the current position of the pointer
* (finger or a leading pointer when there are multiple fingers placed)
* relative to the view attached to the handler.
*/
x: number;
/**
* Y coordinate, expressed in points, of the current position of the pointer
* (finger or a leading pointer when there are multiple fingers placed)
* relative to the view attached to the handler.
*/
y: number;
/**
* X coordinate, expressed in points, of the current position of the pointer
* (finger or a leading pointer when there are multiple fingers placed)
* relative to the window. It is recommended to use `absoluteX` instead of
* `x` in cases when the view attached to the handler can be transformed as an
* effect of the gesture.
*/
absoluteX: number;
/**
* Y coordinate, expressed in points, of the current position of the pointer
* (finger or a leading pointer when there are multiple fingers placed)
* relative to the window. It is recommended to use `absoluteY` instead of
* `y` in cases when the view attached to the handler can be transformed as an
* effect of the gesture.
*/
absoluteY: number;
/**
* Duration of the long press (time since the start of the event), expressed
* in milliseconds.
*/
duration: number;
};
export interface LongPressGestureConfig {
/**
* Minimum time, expressed in milliseconds, that a finger must remain pressed on
* the corresponding view. The default value is 500.
*/
minDurationMs?: number;
/**
* Maximum distance, expressed in points, that defines how far the finger is
* allowed to travel during a long press gesture. If the finger travels
* further than the defined distance and the handler hasn't yet activated, it
* will fail to recognize the gesture. The default value is 10.
*/
maxDist?: number;
}
export interface LongPressGestureHandlerProps
extends BaseGestureHandlerProps<LongPressGestureHandlerEventPayload>,
LongPressGestureConfig {}
export const longPressHandlerName = 'LongPressGestureHandler';
export type LongPressGestureHandler = typeof LongPressGestureHandler;
// eslint-disable-next-line @typescript-eslint/no-redeclare -- backward compatibility; see description on the top of gestureHandlerCommon.ts file
export const LongPressGestureHandler = createHandler<
LongPressGestureHandlerProps,
LongPressGestureHandlerEventPayload
>({
name: longPressHandlerName,
allowedProps: [
...baseGestureHandlerProps,
...longPressGestureHandlerProps,
] as const,
config: {
shouldCancelWhenOutside: true,
},
});

View File

@@ -0,0 +1,55 @@
import createHandler from './createHandler';
import {
BaseGestureHandlerProps,
baseGestureHandlerProps,
} from './gestureHandlerCommon';
export const nativeViewGestureHandlerProps = [
'shouldActivateOnStart',
'disallowInterruption',
] as const;
export interface NativeViewGestureConfig {
/**
* Android only.
*
* Determines whether the handler should check for an existing touch event on
* instantiation.
*/
shouldActivateOnStart?: boolean;
/**
* When `true`, cancels all other gesture handlers when this
* `NativeViewGestureHandler` receives an `ACTIVE` state event.
*/
disallowInterruption?: boolean;
}
export interface NativeViewGestureHandlerProps
extends BaseGestureHandlerProps<NativeViewGestureHandlerPayload>,
NativeViewGestureConfig {}
export type NativeViewGestureHandlerPayload = {
/**
* True if gesture was performed inside of containing view, false otherwise.
*/
pointerInside: boolean;
};
export const nativeViewProps = [
...baseGestureHandlerProps,
...nativeViewGestureHandlerProps,
] as const;
export const nativeViewHandlerName = 'NativeViewGestureHandler';
export type NativeViewGestureHandler = typeof NativeViewGestureHandler;
// eslint-disable-next-line @typescript-eslint/no-redeclare -- backward compatibility; see description on the top of gestureHandlerCommon.ts file
export const NativeViewGestureHandler = createHandler<
NativeViewGestureHandlerProps,
NativeViewGestureHandlerPayload
>({
name: nativeViewHandlerName,
allowedProps: nativeViewProps,
config: {},
});

View File

@@ -0,0 +1,329 @@
import createHandler from './createHandler';
import {
BaseGestureHandlerProps,
baseGestureHandlerProps,
} from './gestureHandlerCommon';
export const panGestureHandlerProps = [
'activeOffsetY',
'activeOffsetX',
'failOffsetY',
'failOffsetX',
'minDist',
'minVelocity',
'minVelocityX',
'minVelocityY',
'minPointers',
'maxPointers',
'avgTouches',
'enableTrackpadTwoFingerGesture',
'activateAfterLongPress',
] as const;
export const panGestureHandlerCustomNativeProps = [
'activeOffsetYStart',
'activeOffsetYEnd',
'activeOffsetXStart',
'activeOffsetXEnd',
'failOffsetYStart',
'failOffsetYEnd',
'failOffsetXStart',
'failOffsetXEnd',
] as const;
export type PanGestureHandlerEventPayload = {
/**
* X coordinate of the current position of the pointer (finger or a leading
* pointer when there are multiple fingers placed) relative to the view
* attached to the handler. Expressed in point units.
*/
x: number;
/**
* Y coordinate of the current position of the pointer (finger or a leading
* pointer when there are multiple fingers placed) relative to the view
* attached to the handler. Expressed in point units.
*/
y: number;
/**
* X coordinate of the current position of the pointer (finger or a leading
* pointer when there are multiple fingers placed) relative to the window.
* The value is expressed in point units. It is recommended to use it instead
* of `x` in cases when the original view can be transformed as an effect of
* the gesture.
*/
absoluteX: number;
/**
* Y coordinate of the current position of the pointer (finger or a leading
* pointer when there are multiple fingers placed) relative to the window.
* The value is expressed in point units. It is recommended to use it instead
* of `y` in cases when the original view can be transformed as an
* effect of the gesture.
*/
absoluteY: number;
/**
* Translation of the pan gesture along X axis accumulated over the time of
* the gesture. The value is expressed in the point units.
*/
translationX: number;
/**
* Translation of the pan gesture along Y axis accumulated over the time of
* the gesture. The value is expressed in the point units.
*/
translationY: number;
/**
* Velocity of the pan gesture along the X axis in the current moment. The
* value is expressed in point units per second.
*/
velocityX: number;
/**
* Velocity of the pan gesture along the Y axis in the current moment. The
* value is expressed in point units per second.
*/
velocityY: number;
};
interface CommonPanProperties {
/**
* Minimum distance the finger (or multiple finger) need to travel before the
* handler activates. Expressed in points.
*/
minDist?: number;
/**
* Android only.
*/
avgTouches?: boolean;
/**
* Enables two-finger gestures on supported devices, for example iPads with
* trackpads. If not enabled the gesture will require click + drag, with
* enableTrackpadTwoFingerGesture swiping with two fingers will also trigger
* the gesture.
*/
enableTrackpadTwoFingerGesture?: boolean;
/**
* A number of fingers that is required to be placed before handler can
* activate. Should be a higher or equal to 0 integer.
*/
minPointers?: number;
/**
* When the given number of fingers is placed on the screen and handler hasn't
* yet activated it will fail recognizing the gesture. Should be a higher or
* equal to 0 integer.
*/
maxPointers?: number;
minVelocity?: number;
minVelocityX?: number;
minVelocityY?: number;
activateAfterLongPress?: number;
}
export interface PanGestureConfig extends CommonPanProperties {
activeOffsetYStart?: number;
activeOffsetYEnd?: number;
activeOffsetXStart?: number;
activeOffsetXEnd?: number;
failOffsetYStart?: number;
failOffsetYEnd?: number;
failOffsetXStart?: number;
failOffsetXEnd?: number;
}
export interface PanGestureHandlerProps
extends BaseGestureHandlerProps<PanGestureHandlerEventPayload>,
CommonPanProperties {
/**
* Range along X axis (in points) where fingers travels without activation of
* handler. Moving outside of this range implies activation of handler. Range
* can be given as an array or a single number. If range is set as an array,
* first value must be lower or equal to 0, a the second one higher or equal
* to 0. If only one number `p` is given a range of `(-inf, p)` will be used
* if `p` is higher or equal to 0 and `(-p, inf)` otherwise.
*/
activeOffsetY?:
| number
| [activeOffsetYStart: number, activeOffsetYEnd: number];
/**
* Range along X axis (in points) where fingers travels without activation of
* handler. Moving outside of this range implies activation of handler. Range
* can be given as an array or a single number. If range is set as an array,
* first value must be lower or equal to 0, a the second one higher or equal
* to 0. If only one number `p` is given a range of `(-inf, p)` will be used
* if `p` is higher or equal to 0 and `(-p, inf)` otherwise.
*/
activeOffsetX?:
| number
| [activeOffsetXStart: number, activeOffsetXEnd: number];
/**
* When the finger moves outside this range (in points) along Y axis and
* handler hasn't yet activated it will fail recognizing the gesture. Range
* can be given as an array or a single number. If range is set as an array,
* first value must be lower or equal to 0, a the second one higher or equal
* to 0. If only one number `p` is given a range of `(-inf, p)` will be used
* if `p` is higher or equal to 0 and `(-p, inf)` otherwise.
*/
failOffsetY?: number | [failOffsetYStart: number, failOffsetYEnd: number];
/**
* When the finger moves outside this range (in points) along X axis and
* handler hasn't yet activated it will fail recognizing the gesture. Range
* can be given as an array or a single number. If range is set as an array,
* first value must be lower or equal to 0, a the second one higher or equal
* to 0. If only one number `p` is given a range of `(-inf, p)` will be used
* if `p` is higher or equal to 0 and `(-p, inf)` otherwise.
*/
failOffsetX?: number | [failOffsetXStart: number, failOffsetXEnd: number];
}
export const panHandlerName = 'PanGestureHandler';
export type PanGestureHandler = typeof PanGestureHandler;
// eslint-disable-next-line @typescript-eslint/no-redeclare -- backward compatibility; see description on the top of gestureHandlerCommon.ts file
export const PanGestureHandler = createHandler<
PanGestureHandlerProps,
PanGestureHandlerEventPayload
>({
name: panHandlerName,
allowedProps: [
...baseGestureHandlerProps,
...panGestureHandlerProps,
] as const,
config: {},
transformProps: managePanProps,
customNativeProps: panGestureHandlerCustomNativeProps,
});
function validatePanGestureHandlerProps(props: PanGestureHandlerProps) {
if (
Array.isArray(props.activeOffsetX) &&
(props.activeOffsetX[0] > 0 || props.activeOffsetX[1] < 0)
) {
throw new Error(
`First element of activeOffsetX should be negative, a the second one should be positive`
);
}
if (
Array.isArray(props.activeOffsetY) &&
(props.activeOffsetY[0] > 0 || props.activeOffsetY[1] < 0)
) {
throw new Error(
`First element of activeOffsetY should be negative, a the second one should be positive`
);
}
if (
Array.isArray(props.failOffsetX) &&
(props.failOffsetX[0] > 0 || props.failOffsetX[1] < 0)
) {
throw new Error(
`First element of failOffsetX should be negative, a the second one should be positive`
);
}
if (
Array.isArray(props.failOffsetY) &&
(props.failOffsetY[0] > 0 || props.failOffsetY[1] < 0)
) {
throw new Error(
`First element of failOffsetY should be negative, a the second one should be positive`
);
}
if (props.minDist && (props.failOffsetX || props.failOffsetY)) {
throw new Error(
`It is not supported to use minDist with failOffsetX or failOffsetY, use activeOffsetX and activeOffsetY instead`
);
}
if (props.minDist && (props.activeOffsetX || props.activeOffsetY)) {
throw new Error(
`It is not supported to use minDist with activeOffsetX or activeOffsetY`
);
}
}
function transformPanGestureHandlerProps(props: PanGestureHandlerProps) {
type InternalPanGHKeys =
| 'activeOffsetXStart'
| 'activeOffsetXEnd'
| 'failOffsetXStart'
| 'failOffsetXEnd'
| 'activeOffsetYStart'
| 'activeOffsetYEnd'
| 'failOffsetYStart'
| 'failOffsetYEnd';
type PanGestureHandlerInternalProps = PanGestureHandlerProps &
Partial<Record<InternalPanGHKeys, number>>;
const res: PanGestureHandlerInternalProps = { ...props };
if (props.activeOffsetX !== undefined) {
delete res.activeOffsetX;
if (Array.isArray(props.activeOffsetX)) {
res.activeOffsetXStart = props.activeOffsetX[0];
res.activeOffsetXEnd = props.activeOffsetX[1];
} else if (props.activeOffsetX < 0) {
res.activeOffsetXStart = props.activeOffsetX;
} else {
res.activeOffsetXEnd = props.activeOffsetX;
}
}
if (props.activeOffsetY !== undefined) {
delete res.activeOffsetY;
if (Array.isArray(props.activeOffsetY)) {
res.activeOffsetYStart = props.activeOffsetY[0];
res.activeOffsetYEnd = props.activeOffsetY[1];
} else if (props.activeOffsetY < 0) {
res.activeOffsetYStart = props.activeOffsetY;
} else {
res.activeOffsetYEnd = props.activeOffsetY;
}
}
if (props.failOffsetX !== undefined) {
delete res.failOffsetX;
if (Array.isArray(props.failOffsetX)) {
res.failOffsetXStart = props.failOffsetX[0];
res.failOffsetXEnd = props.failOffsetX[1];
} else if (props.failOffsetX < 0) {
res.failOffsetXStart = props.failOffsetX;
} else {
res.failOffsetXEnd = props.failOffsetX;
}
}
if (props.failOffsetY !== undefined) {
delete res.failOffsetY;
if (Array.isArray(props.failOffsetY)) {
res.failOffsetYStart = props.failOffsetY[0];
res.failOffsetYEnd = props.failOffsetY[1];
} else if (props.failOffsetY < 0) {
res.failOffsetYStart = props.failOffsetY;
} else {
res.failOffsetYEnd = props.failOffsetY;
}
}
return res;
}
export function managePanProps(props: PanGestureHandlerProps) {
if (__DEV__) {
validatePanGestureHandlerProps(props);
}
return transformPanGestureHandlerProps(props);
}

View File

@@ -0,0 +1,48 @@
import createHandler from './createHandler';
import {
BaseGestureHandlerProps,
baseGestureHandlerProps,
} from './gestureHandlerCommon';
export type PinchGestureHandlerEventPayload = {
/**
* The scale factor relative to the points of the two touches in screen
* coordinates.
*/
scale: number;
/**
* Position expressed in points along X axis of center anchor point of
* gesture.
*/
focalX: number;
/**
* Position expressed in points along Y axis of center anchor point of
* gesture.
*/
focalY: number;
/**
*
* Velocity of the pan gesture the current moment. The value is expressed in
* point units per second.
*/
velocity: number;
};
export interface PinchGestureHandlerProps
extends BaseGestureHandlerProps<PinchGestureHandlerEventPayload> {}
export const pinchHandlerName = 'PinchGestureHandler';
export type PinchGestureHandler = typeof PinchGestureHandler;
// eslint-disable-next-line @typescript-eslint/no-redeclare -- backward compatibility; see description on the top of gestureHandlerCommon.ts file
export const PinchGestureHandler = createHandler<
PinchGestureHandlerProps,
PinchGestureHandlerEventPayload
>({
name: pinchHandlerName,
allowedProps: baseGestureHandlerProps,
config: {},
});

View File

@@ -0,0 +1,2 @@
// @ts-ignore it's not exported so we need to import it from path
export { PressabilityDebugView } from 'react-native/Libraries/Pressability/PressabilityDebug';

View File

@@ -0,0 +1,4 @@
// PressabilityDebugView is not implemented in react-native-web
export function PressabilityDebugView() {
return null;
}

View File

@@ -0,0 +1,48 @@
import createHandler from './createHandler';
import {
BaseGestureHandlerProps,
baseGestureHandlerProps,
} from './gestureHandlerCommon';
export type RotationGestureHandlerEventPayload = {
/**
* Amount rotated, expressed in radians, from the gesture's focal point
* (anchor).
*/
rotation: number;
/**
* X coordinate, expressed in points, of the gesture's central focal point
* (anchor).
*/
anchorX: number;
/**
* Y coordinate, expressed in points, of the gesture's central focal point
* (anchor).
*/
anchorY: number;
/**
*
* Instantaneous velocity, expressed in point units per second, of the
* gesture.
*/
velocity: number;
};
export interface RotationGestureHandlerProps
extends BaseGestureHandlerProps<RotationGestureHandlerEventPayload> {}
export const rotationHandlerName = 'RotationGestureHandler';
export type RotationGestureHandler = typeof RotationGestureHandler;
// eslint-disable-next-line @typescript-eslint/no-redeclare -- backward compatibility; see description on the top of gestureHandlerCommon.ts file
export const RotationGestureHandler = createHandler<
RotationGestureHandlerProps,
RotationGestureHandlerEventPayload
>({
name: rotationHandlerName,
allowedProps: baseGestureHandlerProps,
config: {},
});

View File

@@ -0,0 +1,94 @@
import createHandler from './createHandler';
import {
BaseGestureHandlerProps,
baseGestureHandlerProps,
} from './gestureHandlerCommon';
export const tapGestureHandlerProps = [
'maxDurationMs',
'maxDelayMs',
'numberOfTaps',
'maxDeltaX',
'maxDeltaY',
'maxDist',
'minPointers',
] as const;
export type TapGestureHandlerEventPayload = {
x: number;
y: number;
absoluteX: number;
absoluteY: number;
};
export interface TapGestureConfig {
/**
* Minimum number of pointers (fingers) required to be placed before the
* handler activates. Should be a positive integer.
* The default value is 1.
*/
minPointers?: number;
/**
* Maximum time, expressed in milliseconds, that defines how fast a finger
* must be released after a touch. The default value is 500.
*/
maxDurationMs?: number;
/**
* Maximum time, expressed in milliseconds, that can pass before the next tap
* if many taps are required. The default value is 500.
*/
maxDelayMs?: number;
/**
* Number of tap gestures required to activate the handler. The default value
* is 1.
*/
numberOfTaps?: number;
/**
* Maximum distance, expressed in points, that defines how far the finger is
* allowed to travel along the X axis during a tap gesture. If the finger
* travels further than the defined distance along the X axis and the handler
* hasn't yet activated, it will fail to recognize the gesture.
*/
maxDeltaX?: number;
/**
* Maximum distance, expressed in points, that defines how far the finger is
* allowed to travel along the Y axis during a tap gesture. If the finger
* travels further than the defined distance along the Y axis and the handler
* hasn't yet activated, it will fail to recognize the gesture.
*/
maxDeltaY?: number;
/**
* Maximum distance, expressed in points, that defines how far the finger is
* allowed to travel during a tap gesture. If the finger travels further than
* the defined distance and the handler hasn't yet
* activated, it will fail to recognize the gesture.
*/
maxDist?: number;
}
export interface TapGestureHandlerProps
extends BaseGestureHandlerProps<TapGestureHandlerEventPayload>,
TapGestureConfig {}
export const tapHandlerName = 'TapGestureHandler';
export type TapGestureHandler = typeof TapGestureHandler;
// eslint-disable-next-line @typescript-eslint/no-redeclare -- backward compatibility; see description on the top of gestureHandlerCommon.ts file
export const TapGestureHandler = createHandler<
TapGestureHandlerProps,
TapGestureHandlerEventPayload
>({
name: tapHandlerName,
allowedProps: [
...baseGestureHandlerProps,
...tapGestureHandlerProps,
] as const,
config: {
shouldCancelWhenOutside: true,
},
});

View File

@@ -0,0 +1,549 @@
import * as React from 'react';
import {
Platform,
UIManager,
DeviceEventEmitter,
EmitterSubscription,
} from 'react-native';
import { customDirectEventTypes } from './customDirectEventTypes';
// @ts-ignore - it isn't typed by TS & don't have definitelyTyped types
import deepEqual from 'lodash/isEqual';
import RNGestureHandlerModule from '../RNGestureHandlerModule';
import { State } from '../State';
import {
handlerIDToTag,
getNextHandlerTag,
registerOldGestureHandler,
} from './handlersRegistry';
import {
BaseGestureHandlerProps,
filterConfig,
GestureEvent,
HandlerStateChangeEvent,
findNodeHandle,
scheduleFlushOperations,
} from './gestureHandlerCommon';
import { ValueOf } from '../typeUtils';
import { isFabric, isJestEnv, tagMessage } from '../utils';
import { ActionType } from '../ActionType';
import { PressabilityDebugView } from './PressabilityDebugView';
import GestureHandlerRootViewContext from '../GestureHandlerRootViewContext';
import { ghQueueMicrotask } from '../ghQueueMicrotask';
const UIManagerAny = UIManager as any;
customDirectEventTypes.topGestureHandlerEvent = {
registrationName: 'onGestureHandlerEvent',
};
const customGHEventsConfigFabricAndroid = {
topOnGestureHandlerEvent: { registrationName: 'onGestureHandlerEvent' },
topOnGestureHandlerStateChange: {
registrationName: 'onGestureHandlerStateChange',
},
};
const customGHEventsConfig = {
onGestureHandlerEvent: { registrationName: 'onGestureHandlerEvent' },
onGestureHandlerStateChange: {
registrationName: 'onGestureHandlerStateChange',
},
// When using React Native Gesture Handler for Animated.event with useNativeDriver: true
// on Android with Fabric enabled, the native part still sends the native events to JS
// but prefixed with "top". We cannot simply rename the events above so they are prefixed
// with "top" instead of "on" because in such case Animated.events would not be registered.
// That's why we need to register another pair of event names.
// The incoming events will be queued but never handled.
// Without this piece of code below, you'll get the following JS error:
// Unsupported top level event type "topOnGestureHandlerEvent" dispatched
...(isFabric() &&
Platform.OS === 'android' &&
customGHEventsConfigFabricAndroid),
};
// Add gesture specific events to genericDirectEventTypes object exported from UIManager
// native module.
// Once new event types are registered with react it is possible to dispatch these
// events to all kind of native views.
UIManagerAny.genericDirectEventTypes = {
...UIManagerAny.genericDirectEventTypes,
...customGHEventsConfig,
};
// In newer versions of RN the `genericDirectEventTypes` is located in the object
// returned by UIManager.getViewManagerConfig('getConstants') or in older RN UIManager.getConstants(), we need to add it there as well to make
// it compatible with RN 61+
const UIManagerConstants =
UIManagerAny.getViewManagerConfig?.('getConstants') ??
UIManagerAny.getConstants?.();
if (UIManagerConstants) {
UIManagerConstants.genericDirectEventTypes = {
...UIManagerConstants.genericDirectEventTypes,
...customGHEventsConfig,
};
}
// Wrap JS responder calls and notify gesture handler manager
const {
setJSResponder: oldSetJSResponder = () => {
//no operation
},
clearJSResponder: oldClearJSResponder = () => {
//no operation
},
} = UIManagerAny;
UIManagerAny.setJSResponder = (tag: number, blockNativeResponder: boolean) => {
RNGestureHandlerModule.handleSetJSResponder(tag, blockNativeResponder);
oldSetJSResponder(tag, blockNativeResponder);
};
UIManagerAny.clearJSResponder = () => {
RNGestureHandlerModule.handleClearJSResponder();
oldClearJSResponder();
};
let allowTouches = true;
const DEV_ON_ANDROID = __DEV__ && Platform.OS === 'android';
// Toggled inspector blocks touch events in order to allow inspecting on Android
// This needs to be a global variable in order to set initial state for `allowTouches` property in Handler component
if (DEV_ON_ANDROID) {
DeviceEventEmitter.addListener('toggleElementInspector', () => {
allowTouches = !allowTouches;
});
}
type HandlerProps<T extends Record<string, unknown>> = Readonly<
React.PropsWithChildren<BaseGestureHandlerProps<T>>
>;
function hasUnresolvedRefs<T extends Record<string, unknown>>(
props: HandlerProps<T>
) {
// TODO(TS) - add type for extract arg
const extract = (refs: any | any[]) => {
if (!Array.isArray(refs)) {
return refs && refs.current === null;
}
return refs.some((r) => r && r.current === null);
};
return extract(props['simultaneousHandlers']) || extract(props['waitFor']);
}
const stateToPropMappings = {
[State.UNDETERMINED]: undefined,
[State.BEGAN]: 'onBegan',
[State.FAILED]: 'onFailed',
[State.CANCELLED]: 'onCancelled',
[State.ACTIVE]: 'onActivated',
[State.END]: 'onEnded',
} as const;
type CreateHandlerArgs<HandlerPropsT extends Record<string, unknown>> =
Readonly<{
name: string;
allowedProps: Readonly<Extract<keyof HandlerPropsT, string>[]>;
config: Readonly<Record<string, unknown>>;
transformProps?: (props: HandlerPropsT) => HandlerPropsT;
customNativeProps?: Readonly<string[]>;
}>;
// TODO(TS) fix event types
type InternalEventHandlers = {
onGestureHandlerEvent?: (event: any) => void;
onGestureHandlerStateChange?: (event: any) => void;
};
type AttachGestureHandlerWeb = (
handlerTag: number,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
newView: any,
_actionType: ActionType,
propsRef: React.RefObject<unknown>
) => void;
const UNRESOLVED_REFS_RETRY_LIMIT = 1;
// TODO(TS) - make sure that BaseGestureHandlerProps doesn't need other generic parameter to work with custom properties.
export default function createHandler<
T extends BaseGestureHandlerProps<U>,
U extends Record<string, unknown>
>({
name,
allowedProps = [],
config = {},
transformProps,
customNativeProps = [],
}: CreateHandlerArgs<T>): React.ComponentType<T & React.RefAttributes<any>> {
interface HandlerState {
allowTouches: boolean;
}
class Handler extends React.Component<
T & InternalEventHandlers,
HandlerState
> {
static displayName = name;
static contextType = GestureHandlerRootViewContext;
private handlerTag: number;
private config: Record<string, unknown>;
private propsRef: React.MutableRefObject<unknown>;
private isMountedRef: React.MutableRefObject<boolean | null>;
private viewNode: any;
private viewTag?: number;
private inspectorToggleListener?: EmitterSubscription;
constructor(props: T & InternalEventHandlers) {
super(props);
this.handlerTag = getNextHandlerTag();
this.config = {};
this.propsRef = React.createRef();
this.isMountedRef = React.createRef();
this.state = { allowTouches };
if (props.id) {
if (handlerIDToTag[props.id] !== undefined) {
throw new Error(`Handler with ID "${props.id}" already registered`);
}
handlerIDToTag[props.id] = this.handlerTag;
}
}
componentDidMount() {
const props: HandlerProps<U> = this.props;
this.isMountedRef.current = true;
if (DEV_ON_ANDROID) {
this.inspectorToggleListener = DeviceEventEmitter.addListener(
'toggleElementInspector',
() => {
this.setState((_) => ({ allowTouches }));
this.update(UNRESOLVED_REFS_RETRY_LIMIT);
}
);
}
if (hasUnresolvedRefs(props)) {
// If there are unresolved refs (e.g. ".current" has not yet been set)
// passed as `simultaneousHandlers` or `waitFor`, we enqueue a call to
// _update method that will try to update native handler props using
// queueMicrotask. This makes it so update() function gets called after all
// react components are mounted and we expect the missing ref object to
// be resolved by then.
ghQueueMicrotask(() => {
this.update(UNRESOLVED_REFS_RETRY_LIMIT);
});
}
this.createGestureHandler(
filterConfig(
transformProps ? transformProps(this.props) : this.props,
[...allowedProps, ...customNativeProps],
config
)
);
this.attachGestureHandler(findNodeHandle(this.viewNode) as number); // TODO(TS) - check if this can be null
}
componentDidUpdate() {
const viewTag = findNodeHandle(this.viewNode);
if (this.viewTag !== viewTag) {
this.attachGestureHandler(viewTag as number); // TODO(TS) - check interaction between _viewTag & findNodeHandle
}
this.update(UNRESOLVED_REFS_RETRY_LIMIT);
}
componentWillUnmount() {
this.inspectorToggleListener?.remove();
this.isMountedRef.current = false;
RNGestureHandlerModule.dropGestureHandler(this.handlerTag);
scheduleFlushOperations();
// We can't use this.props.id directly due to TS generic type narrowing bug, see https://github.com/microsoft/TypeScript/issues/13995 for more context
const handlerID: string | undefined = this.props.id;
if (handlerID) {
// eslint-disable-next-line @typescript-eslint/no-dynamic-delete
delete handlerIDToTag[handlerID];
}
}
private onGestureHandlerEvent = (event: GestureEvent<U>) => {
if (event.nativeEvent.handlerTag === this.handlerTag) {
if (typeof this.props.onGestureEvent === 'function') {
this.props.onGestureEvent?.(event);
}
} else {
this.props.onGestureHandlerEvent?.(event);
}
};
// TODO(TS) - make sure this is right type for event
private onGestureHandlerStateChange = (
event: HandlerStateChangeEvent<U>
) => {
if (event.nativeEvent.handlerTag === this.handlerTag) {
if (typeof this.props.onHandlerStateChange === 'function') {
this.props.onHandlerStateChange?.(event);
}
const state: ValueOf<typeof State> = event.nativeEvent.state;
const stateEventName = stateToPropMappings[state];
const eventHandler = stateEventName && this.props[stateEventName];
if (eventHandler && typeof eventHandler === 'function') {
eventHandler(event);
}
} else {
this.props.onGestureHandlerStateChange?.(event);
}
};
private refHandler = (node: any) => {
this.viewNode = node;
const child = React.Children.only(this.props.children);
// TODO(TS) fix ref type
const { ref }: any = child;
if (ref !== null) {
if (typeof ref === 'function') {
ref(node);
} else {
ref.current = node;
}
}
};
private createGestureHandler = (
newConfig: Readonly<Record<string, unknown>>
) => {
this.config = newConfig;
RNGestureHandlerModule.createGestureHandler(
name,
this.handlerTag,
newConfig
);
};
private attachGestureHandler = (newViewTag: number) => {
this.viewTag = newViewTag;
if (Platform.OS === 'web') {
// typecast due to dynamic resolution, attachGestureHandler should have web version signature in this branch
(
RNGestureHandlerModule.attachGestureHandler as AttachGestureHandlerWeb
)(
this.handlerTag,
newViewTag,
ActionType.JS_FUNCTION_OLD_API, // ignored on web
this.propsRef
);
} else {
registerOldGestureHandler(this.handlerTag, {
onGestureEvent: this.onGestureHandlerEvent,
onGestureStateChange: this.onGestureHandlerStateChange,
});
const actionType = (() => {
const onGestureEvent = this.props?.onGestureEvent;
const isGestureHandlerWorklet =
onGestureEvent &&
('current' in onGestureEvent ||
'workletEventHandler' in onGestureEvent);
const onHandlerStateChange = this.props?.onHandlerStateChange;
const isStateChangeHandlerWorklet =
onHandlerStateChange &&
('current' in onHandlerStateChange ||
'workletEventHandler' in onHandlerStateChange);
const isReanimatedHandler =
isGestureHandlerWorklet || isStateChangeHandlerWorklet;
if (isReanimatedHandler) {
// Reanimated worklet
return ActionType.REANIMATED_WORKLET;
} else if (onGestureEvent && '__isNative' in onGestureEvent) {
// Animated.event with useNativeDriver: true
return ActionType.NATIVE_ANIMATED_EVENT;
} else {
// JS callback or Animated.event with useNativeDriver: false
return ActionType.JS_FUNCTION_OLD_API;
}
})();
RNGestureHandlerModule.attachGestureHandler(
this.handlerTag,
newViewTag,
actionType
);
}
scheduleFlushOperations();
};
private updateGestureHandler = (
newConfig: Readonly<Record<string, unknown>>
) => {
this.config = newConfig;
RNGestureHandlerModule.updateGestureHandler(this.handlerTag, newConfig);
scheduleFlushOperations();
};
private update(remainingTries: number) {
if (!this.isMountedRef.current) {
return;
}
const props: HandlerProps<U> = this.props;
// When ref is set via a function i.e. `ref={(r) => refObject.current = r}` instead of
// `ref={refObject}` it's possible that it won't be resolved in time. Seems like trying
// again is easy enough fix.
if (hasUnresolvedRefs(props) && remainingTries > 0) {
ghQueueMicrotask(() => {
this.update(remainingTries - 1);
});
} else {
const newConfig = filterConfig(
transformProps ? transformProps(this.props) : this.props,
[...allowedProps, ...customNativeProps],
config
);
if (!deepEqual(this.config, newConfig)) {
this.updateGestureHandler(newConfig);
}
}
}
setNativeProps(updates: any) {
const mergedProps = { ...this.props, ...updates };
const newConfig = filterConfig(
transformProps ? transformProps(mergedProps) : mergedProps,
[...allowedProps, ...customNativeProps],
config
);
this.updateGestureHandler(newConfig);
}
render() {
if (__DEV__ && !this.context && !isJestEnv() && Platform.OS !== 'web') {
throw new Error(
name +
' must be used as a descendant of GestureHandlerRootView. Otherwise the gestures will not be recognized. See https://docs.swmansion.com/react-native-gesture-handler/docs/installation for more details.'
);
}
let gestureEventHandler = this.onGestureHandlerEvent;
// Another instance of https://github.com/microsoft/TypeScript/issues/13995
type OnGestureEventHandlers = {
onGestureEvent?: BaseGestureHandlerProps<U>['onGestureEvent'];
onGestureHandlerEvent?: InternalEventHandlers['onGestureHandlerEvent'];
};
const { onGestureEvent, onGestureHandlerEvent }: OnGestureEventHandlers =
this.props;
if (onGestureEvent && typeof onGestureEvent !== 'function') {
// If it's not a method it should be an native Animated.event
// object. We set it directly as the handler for the view
// In this case nested handlers are not going to be supported
if (onGestureHandlerEvent) {
throw new Error(
'Nesting touch handlers with native animated driver is not supported yet'
);
}
gestureEventHandler = onGestureEvent;
} else {
if (
onGestureHandlerEvent &&
typeof onGestureHandlerEvent !== 'function'
) {
throw new Error(
'Nesting touch handlers with native animated driver is not supported yet'
);
}
}
let gestureStateEventHandler = this.onGestureHandlerStateChange;
// Another instance of https://github.com/microsoft/TypeScript/issues/13995
type OnGestureStateChangeHandlers = {
onHandlerStateChange?: BaseGestureHandlerProps<U>['onHandlerStateChange'];
onGestureHandlerStateChange?: InternalEventHandlers['onGestureHandlerStateChange'];
};
const {
onHandlerStateChange,
onGestureHandlerStateChange,
}: OnGestureStateChangeHandlers = this.props;
if (onHandlerStateChange && typeof onHandlerStateChange !== 'function') {
// If it's not a method it should be an native Animated.event
// object. We set it directly as the handler for the view
// In this case nested handlers are not going to be supported
if (onGestureHandlerStateChange) {
throw new Error(
'Nesting touch handlers with native animated driver is not supported yet'
);
}
gestureStateEventHandler = onHandlerStateChange;
} else {
if (
onGestureHandlerStateChange &&
typeof onGestureHandlerStateChange !== 'function'
) {
throw new Error(
'Nesting touch handlers with native animated driver is not supported yet'
);
}
}
const events = {
onGestureHandlerEvent: this.state.allowTouches
? gestureEventHandler
: undefined,
onGestureHandlerStateChange: this.state.allowTouches
? gestureStateEventHandler
: undefined,
};
this.propsRef.current = events;
let child: any = null;
try {
child = React.Children.only(this.props.children);
} catch (e) {
throw new Error(
tagMessage(
`${name} got more than one view as a child. If you want the gesture to work on multiple views, wrap them with a common parent and attach the gesture to that view.`
)
);
}
let grandChildren = child.props.children;
if (
__DEV__ &&
child.type &&
(child.type === 'RNGestureHandlerButton' ||
child.type.name === 'View' ||
child.type.displayName === 'View')
) {
grandChildren = React.Children.toArray(grandChildren);
grandChildren.push(
<PressabilityDebugView
key="pressabilityDebugView"
color="mediumspringgreen"
hitSlop={child.props.hitSlop}
/>
);
}
return React.cloneElement(
child,
{
ref: this.refHandler,
collapsable: false,
...(isJestEnv()
? {
handlerType: name,
handlerTag: this.handlerTag,
}
: {}),
testID: this.props.testID ?? child.props.testID,
...events,
},
grandChildren
);
}
}
return Handler;
}

View File

@@ -0,0 +1,80 @@
import * as React from 'react';
import { useImperativeHandle, useRef } from 'react';
import {
NativeViewGestureHandler,
NativeViewGestureHandlerProps,
nativeViewProps,
} from './NativeViewGestureHandler';
/*
* This array should consist of:
* - All keys in propTypes from NativeGestureHandler
* (and all keys in GestureHandlerPropTypes)
* - 'onGestureHandlerEvent'
* - 'onGestureHandlerStateChange'
*/
const NATIVE_WRAPPER_PROPS_FILTER = [
...nativeViewProps,
'onGestureHandlerEvent',
'onGestureHandlerStateChange',
] as const;
export default function createNativeWrapper<P>(
Component: React.ComponentType<P>,
config: Readonly<NativeViewGestureHandlerProps> = {}
) {
const ComponentWrapper = React.forwardRef<
React.ComponentType<any>,
P & NativeViewGestureHandlerProps
>((props, ref) => {
// filter out props that should be passed to gesture handler wrapper
const gestureHandlerProps = Object.keys(props).reduce(
(res, key) => {
// TS being overly protective with it's types, see https://github.com/microsoft/TypeScript/issues/26255#issuecomment-458013731 for more info
const allowedKeys: readonly string[] = NATIVE_WRAPPER_PROPS_FILTER;
if (allowedKeys.includes(key)) {
// @ts-ignore FIXME(TS)
res[key] = props[key];
}
return res;
},
{ ...config } // watch out not to modify config
);
const _ref = useRef<React.ComponentType<P>>();
const _gestureHandlerRef = useRef<React.ComponentType<P>>();
useImperativeHandle(
ref,
// @ts-ignore TODO(TS) decide how nulls work in this context
() => {
const node = _gestureHandlerRef.current;
// add handlerTag for relations config
if (_ref.current && node) {
// @ts-ignore FIXME(TS) think about createHandler return type
_ref.current.handlerTag = node.handlerTag;
return _ref.current;
}
return null;
},
[_ref, _gestureHandlerRef]
);
return (
<NativeViewGestureHandler
{...gestureHandlerProps}
// @ts-ignore TODO(TS)
ref={_gestureHandlerRef}>
<Component {...props} ref={_ref} />
</NativeViewGestureHandler>
);
});
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
ComponentWrapper.displayName =
Component?.displayName ||
// @ts-ignore if render doesn't exist it will return undefined and go further
Component?.render?.name ||
(typeof Component === 'string' && Component) ||
'ComponentWrapper';
return ComponentWrapper;
}

View File

@@ -0,0 +1,2 @@
// @ts-ignore - its taken straight from RN
export { customDirectEventTypes } from 'react-native/Libraries/Renderer/shims/ReactNativeViewConfigRegistry';

View File

@@ -0,0 +1,5 @@
// customDirectEventTypes doesn't exist in react-native-web, therefore importing it
// directly in createHandler.tsx would end in crash.
const customDirectEventTypes = {};
export { customDirectEventTypes };

View File

@@ -0,0 +1,284 @@
// Previous types exported gesture handlers as classes which creates an interface and variable, both named the same as class.
// Without those types, we'd introduce breaking change, forcing users to prefix every handler type specification with typeof
// e.g. React.createRef<TapGestureHandler> -> React.createRef<typeof TapGestureHandler>.
// See https://www.typescriptlang.org/docs/handbook/classes.html#constructor-functions for reference.
import * as React from 'react';
import { Platform, findNodeHandle as findNodeHandleRN } from 'react-native';
import { State } from '../State';
import { TouchEventType } from '../TouchEventType';
import { ValueOf } from '../typeUtils';
import { handlerIDToTag } from './handlersRegistry';
import { toArray } from '../utils';
import RNGestureHandlerModule from '../RNGestureHandlerModule';
import { ghQueueMicrotask } from '../ghQueueMicrotask';
import { PointerType } from '../PointerType';
const commonProps = [
'id',
'enabled',
'shouldCancelWhenOutside',
'hitSlop',
'cancelsTouchesInView',
'userSelect',
'activeCursor',
'mouseButton',
'enableContextMenu',
'touchAction',
] as const;
const componentInteractionProps = [
'waitFor',
'simultaneousHandlers',
'blocksHandlers',
] as const;
export const baseGestureHandlerProps = [
...commonProps,
...componentInteractionProps,
'onBegan',
'onFailed',
'onCancelled',
'onActivated',
'onEnded',
'onGestureEvent',
'onHandlerStateChange',
] as const;
export const baseGestureHandlerWithMonitorProps = [
...commonProps,
'needsPointerData',
'manualActivation',
];
export interface GestureEventPayload {
handlerTag: number;
numberOfPointers: number;
state: ValueOf<typeof State>;
pointerType: PointerType;
}
export interface HandlerStateChangeEventPayload extends GestureEventPayload {
oldState: ValueOf<typeof State>;
}
export type HitSlop =
| number
| Partial<
Record<
'left' | 'right' | 'top' | 'bottom' | 'vertical' | 'horizontal',
number
>
>
| Record<'width' | 'left', number>
| Record<'width' | 'right', number>
| Record<'height' | 'top', number>
| Record<'height' | 'bottom', number>;
export type UserSelect = 'none' | 'auto' | 'text';
export type ActiveCursor =
| 'auto'
| 'default'
| 'none'
| 'context-menu'
| 'help'
| 'pointer'
| 'progress'
| 'wait'
| 'cell'
| 'crosshair'
| 'text'
| 'vertical-text'
| 'alias'
| 'copy'
| 'move'
| 'no-drop'
| 'not-allowed'
| 'grab'
| 'grabbing'
| 'e-resize'
| 'n-resize'
| 'ne-resize'
| 'nw-resize'
| 's-resize'
| 'se-resize'
| 'sw-resize'
| 'w-resize'
| 'ew-resize'
| 'ns-resize'
| 'nesw-resize'
| 'nwse-resize'
| 'col-resize'
| 'row-resize'
| 'all-scroll'
| 'zoom-in'
| 'zoom-out';
export enum MouseButton {
LEFT = 1,
RIGHT = 2,
MIDDLE = 4,
BUTTON_4 = 8,
BUTTON_5 = 16,
ALL = 31,
}
export type TouchAction =
| 'auto'
| 'none'
| 'pan-x'
| 'pan-left'
| 'pan-right'
| 'pan-y'
| 'pan-up'
| 'pan-down'
| 'pinch-zoom'
| 'manipulation'
| 'inherit'
| 'initial'
| 'revert'
| 'revert-layer'
| 'unset';
//TODO(TS) events in handlers
export interface GestureEvent<ExtraEventPayloadT = Record<string, unknown>> {
nativeEvent: Readonly<GestureEventPayload & ExtraEventPayloadT>;
}
export interface HandlerStateChangeEvent<
ExtraEventPayloadT = Record<string, unknown>
> {
nativeEvent: Readonly<HandlerStateChangeEventPayload & ExtraEventPayloadT>;
}
export type TouchData = {
id: number;
x: number;
y: number;
absoluteX: number;
absoluteY: number;
};
export type GestureTouchEvent = {
handlerTag: number;
numberOfTouches: number;
state: ValueOf<typeof State>;
eventType: TouchEventType;
allTouches: TouchData[];
changedTouches: TouchData[];
};
export type GestureUpdateEvent<GestureEventPayloadT = Record<string, unknown>> =
GestureEventPayload & GestureEventPayloadT;
export type GestureStateChangeEvent<
GestureStateChangeEventPayloadT = Record<string, unknown>
> = HandlerStateChangeEventPayload & GestureStateChangeEventPayloadT;
export type CommonGestureConfig = {
enabled?: boolean;
shouldCancelWhenOutside?: boolean;
hitSlop?: HitSlop;
userSelect?: UserSelect;
activeCursor?: ActiveCursor;
mouseButton?: MouseButton;
enableContextMenu?: boolean;
touchAction?: TouchAction;
};
// Events payloads are types instead of interfaces due to TS limitation.
// See https://github.com/microsoft/TypeScript/issues/15300 for more info.
export type BaseGestureHandlerProps<
ExtraEventPayloadT extends Record<string, unknown> = Record<string, unknown>
> = CommonGestureConfig & {
id?: string;
waitFor?: React.Ref<unknown> | React.Ref<unknown>[];
simultaneousHandlers?: React.Ref<unknown> | React.Ref<unknown>[];
blocksHandlers?: React.Ref<unknown> | React.Ref<unknown>[];
testID?: string;
cancelsTouchesInView?: boolean;
// TODO(TS) - fix event types
onBegan?: (event: HandlerStateChangeEvent) => void;
onFailed?: (event: HandlerStateChangeEvent) => void;
onCancelled?: (event: HandlerStateChangeEvent) => void;
onActivated?: (event: HandlerStateChangeEvent) => void;
onEnded?: (event: HandlerStateChangeEvent) => void;
//TODO(TS) consider using NativeSyntheticEvent
onGestureEvent?: (event: GestureEvent<ExtraEventPayloadT>) => void;
onHandlerStateChange?: (
event: HandlerStateChangeEvent<ExtraEventPayloadT>
) => void;
// implicit `children` prop has been removed in @types/react^18.0.0
children?: React.ReactNode;
};
function isConfigParam(param: unknown, name: string) {
// param !== Object(param) returns false if `param` is a function
// or an object and returns true if `param` is null
return (
param !== undefined &&
(param !== Object(param) ||
!('__isNative' in (param as Record<string, unknown>))) &&
name !== 'onHandlerStateChange' &&
name !== 'onGestureEvent'
);
}
export function filterConfig(
props: Record<string, unknown>,
validProps: string[],
defaults: Record<string, unknown> = {}
) {
const filteredConfig = { ...defaults };
for (const key of validProps) {
let value = props[key];
if (isConfigParam(value, key)) {
if (key === 'simultaneousHandlers' || key === 'waitFor') {
value = transformIntoHandlerTags(props[key]);
} else if (key === 'hitSlop' && typeof value !== 'object') {
value = { top: value, left: value, bottom: value, right: value };
}
filteredConfig[key] = value;
}
}
return filteredConfig;
}
function transformIntoHandlerTags(handlerIDs: any) {
handlerIDs = toArray(handlerIDs);
if (Platform.OS === 'web') {
return handlerIDs
.map(({ current }: { current: any }) => current)
.filter((handle: any) => handle);
}
// converts handler string IDs into their numeric tags
return handlerIDs
.map(
(handlerID: any) =>
handlerIDToTag[handlerID] || handlerID.current?.handlerTag || -1
)
.filter((handlerTag: number) => handlerTag > 0);
}
export function findNodeHandle(
node: null | number | React.Component<any, any> | React.ComponentClass<any>
): null | number | React.Component<any, any> | React.ComponentClass<any> {
if (Platform.OS === 'web') {
return node;
}
return findNodeHandleRN(node);
}
let flushOperationsScheduled = false;
export function scheduleFlushOperations() {
if (!flushOperationsScheduled) {
flushOperationsScheduled = true;
ghQueueMicrotask(() => {
RNGestureHandlerModule.flushOperations();
flushOperationsScheduled = false;
});
}
}

View File

@@ -0,0 +1,106 @@
import {
BaseButtonProps,
BorderlessButtonProps,
RawButtonProps,
RectButtonProps,
} from '../components/GestureButtons';
import {
GestureEvent,
GestureEventPayload,
HandlerStateChangeEvent,
HandlerStateChangeEventPayload,
} from './gestureHandlerCommon';
import {
FlingGestureHandlerEventPayload,
FlingGestureHandlerProps,
} from './FlingGestureHandler';
import {
ForceTouchGestureHandlerEventPayload,
ForceTouchGestureHandlerProps,
} from './ForceTouchGestureHandler';
import {
LongPressGestureHandlerEventPayload,
LongPressGestureHandlerProps,
} from './LongPressGestureHandler';
import {
PanGestureHandlerEventPayload,
PanGestureHandlerProps,
} from './PanGestureHandler';
import {
PinchGestureHandlerEventPayload,
PinchGestureHandlerProps,
} from './PinchGestureHandler';
import {
RotationGestureHandlerEventPayload,
RotationGestureHandlerProps,
} from './RotationGestureHandler';
import {
TapGestureHandlerEventPayload,
TapGestureHandlerProps,
} from './TapGestureHandler';
import {
NativeViewGestureHandlerPayload,
NativeViewGestureHandlerProps,
} from './NativeViewGestureHandler';
// events
export type GestureHandlerGestureEventNativeEvent = GestureEventPayload;
export type GestureHandlerStateChangeNativeEvent =
HandlerStateChangeEventPayload;
export type GestureHandlerGestureEvent = GestureEvent;
export type GestureHandlerStateChangeEvent = HandlerStateChangeEvent;
// gesture handlers events
export type NativeViewGestureHandlerGestureEvent =
GestureEvent<NativeViewGestureHandlerPayload>;
export type NativeViewGestureHandlerStateChangeEvent =
HandlerStateChangeEvent<NativeViewGestureHandlerPayload>;
export type TapGestureHandlerGestureEvent =
GestureEvent<TapGestureHandlerEventPayload>;
export type TapGestureHandlerStateChangeEvent =
HandlerStateChangeEvent<TapGestureHandlerEventPayload>;
export type ForceTouchGestureHandlerGestureEvent =
GestureEvent<ForceTouchGestureHandlerEventPayload>;
export type ForceTouchGestureHandlerStateChangeEvent =
HandlerStateChangeEvent<ForceTouchGestureHandlerEventPayload>;
export type LongPressGestureHandlerGestureEvent =
GestureEvent<LongPressGestureHandlerEventPayload>;
export type LongPressGestureHandlerStateChangeEvent =
HandlerStateChangeEvent<LongPressGestureHandlerEventPayload>;
export type PanGestureHandlerGestureEvent =
GestureEvent<PanGestureHandlerEventPayload>;
export type PanGestureHandlerStateChangeEvent =
HandlerStateChangeEvent<PanGestureHandlerEventPayload>;
export type PinchGestureHandlerGestureEvent =
GestureEvent<PinchGestureHandlerEventPayload>;
export type PinchGestureHandlerStateChangeEvent =
HandlerStateChangeEvent<PinchGestureHandlerEventPayload>;
export type RotationGestureHandlerGestureEvent =
GestureEvent<RotationGestureHandlerEventPayload>;
export type RotationGestureHandlerStateChangeEvent =
HandlerStateChangeEvent<RotationGestureHandlerEventPayload>;
export type FlingGestureHandlerGestureEvent =
GestureEvent<FlingGestureHandlerEventPayload>;
export type FlingGestureHandlerStateChangeEvent =
HandlerStateChangeEvent<FlingGestureHandlerEventPayload>;
// handlers properties
export type NativeViewGestureHandlerProperties = NativeViewGestureHandlerProps;
export type TapGestureHandlerProperties = TapGestureHandlerProps;
export type LongPressGestureHandlerProperties = LongPressGestureHandlerProps;
export type PanGestureHandlerProperties = PanGestureHandlerProps;
export type PinchGestureHandlerProperties = PinchGestureHandlerProps;
export type RotationGestureHandlerProperties = RotationGestureHandlerProps;
export type FlingGestureHandlerProperties = FlingGestureHandlerProps;
export type ForceTouchGestureHandlerProperties = ForceTouchGestureHandlerProps;
// button props
export type RawButtonProperties = RawButtonProps;
export type BaseButtonProperties = BaseButtonProps;
export type RectButtonProperties = RectButtonProps;
export type BorderlessButtonProperties = BorderlessButtonProps;

View File

@@ -0,0 +1,889 @@
import React, { useContext, useEffect, useRef, useState } from 'react';
import {
GestureType,
HandlerCallbacks,
BaseGesture,
GestureRef,
CALLBACK_TYPE,
} from './gesture';
import { Reanimated, SharedValue } from './reanimatedWrapper';
import { registerHandler, unregisterHandler } from '../handlersRegistry';
import RNGestureHandlerModule from '../../RNGestureHandlerModule';
import {
baseGestureHandlerWithMonitorProps,
filterConfig,
findNodeHandle,
GestureTouchEvent,
GestureUpdateEvent,
GestureStateChangeEvent,
HandlerStateChangeEvent,
scheduleFlushOperations,
UserSelect,
TouchAction,
} from '../gestureHandlerCommon';
import {
GestureStateManager,
GestureStateManagerType,
} from './gestureStateManager';
import { flingGestureHandlerProps } from '../FlingGestureHandler';
import { forceTouchGestureHandlerProps } from '../ForceTouchGestureHandler';
import { longPressGestureHandlerProps } from '../LongPressGestureHandler';
import {
panGestureHandlerProps,
panGestureHandlerCustomNativeProps,
} from '../PanGestureHandler';
import { tapGestureHandlerProps } from '../TapGestureHandler';
import { hoverGestureHandlerProps } from './hoverGesture';
import { State } from '../../State';
import { TouchEventType } from '../../TouchEventType';
import { ComposedGesture } from './gestureComposition';
import { ActionType } from '../../ActionType';
import { isFabric, isJestEnv, tagMessage } from '../../utils';
import { getReactNativeVersion } from '../../getReactNativeVersion';
import { getShadowNodeFromRef } from '../../getShadowNodeFromRef';
import { Platform } from 'react-native';
import type RNGestureHandlerModuleWeb from '../../RNGestureHandlerModule.web';
import { onGestureHandlerEvent } from './eventReceiver';
import { RNRenderer } from '../../RNRenderer';
import { isNewWebImplementationEnabled } from '../../EnableNewWebImplementation';
import { nativeViewGestureHandlerProps } from '../NativeViewGestureHandler';
import GestureHandlerRootViewContext from '../../GestureHandlerRootViewContext';
import { ghQueueMicrotask } from '../../ghQueueMicrotask';
declare const global: {
isFormsStackingContext: (node: unknown) => boolean | null; // JSI function
};
const ALLOWED_PROPS = [
...baseGestureHandlerWithMonitorProps,
...tapGestureHandlerProps,
...panGestureHandlerProps,
...panGestureHandlerCustomNativeProps,
...longPressGestureHandlerProps,
...forceTouchGestureHandlerProps,
...flingGestureHandlerProps,
...hoverGestureHandlerProps,
...nativeViewGestureHandlerProps,
];
export type GestureConfigReference = {
config: GestureType[];
animatedEventHandler: unknown;
animatedHandlers: SharedValue<
HandlerCallbacks<Record<string, unknown>>[] | null
> | null;
firstExecution: boolean;
useReanimatedHook: boolean;
};
function convertToHandlerTag(ref: GestureRef): number {
if (typeof ref === 'number') {
return ref;
} else if (ref instanceof BaseGesture) {
return ref.handlerTag;
} else {
// @ts-ignore in this case it should be a ref either to gesture object or
// a gesture handler component, in both cases handlerTag property exists
return ref.current?.handlerTag ?? -1;
}
}
function extractValidHandlerTags(interactionGroup: GestureRef[] | undefined) {
return (
interactionGroup?.map(convertToHandlerTag)?.filter((tag) => tag > 0) ?? []
);
}
function dropHandlers(preparedGesture: GestureConfigReference) {
for (const handler of preparedGesture.config) {
RNGestureHandlerModule.dropGestureHandler(handler.handlerTag);
unregisterHandler(handler.handlerTag, handler.config.testId);
}
scheduleFlushOperations();
}
function checkGestureCallbacksForWorklets(gesture: GestureType) {
// if a gesture is explicitly marked to run on the JS thread there is no need to check
// if callbacks are worklets as the user is aware they will be ran on the JS thread
if (gesture.config.runOnJS) {
return;
}
const areSomeNotWorklets = gesture.handlers.isWorklet.includes(false);
const areSomeWorklets = gesture.handlers.isWorklet.includes(true);
// if some of the callbacks are worklets and some are not, and the gesture is not
// explicitly marked with `.runOnJS(true)` show an error
if (areSomeNotWorklets && areSomeWorklets) {
console.error(
tagMessage(
`Some of the callbacks in the gesture are worklets and some are not. Either make sure that all calbacks are marked as 'worklet' if you wish to run them on the UI thread or use '.runOnJS(true)' modifier on the gesture explicitly to run all callbacks on the JS thread.`
)
);
}
}
interface WebEventHandler {
onGestureHandlerEvent: (event: HandlerStateChangeEvent<unknown>) => void;
onGestureHandlerStateChange?: (
event: HandlerStateChangeEvent<unknown>
) => void;
}
interface AttachHandlersConfig {
preparedGesture: GestureConfigReference;
gestureConfig: ComposedGesture | GestureType;
gesture: GestureType[];
viewTag: number;
webEventHandlersRef: React.RefObject<WebEventHandler>;
mountedRef: React.RefObject<boolean>;
}
function attachHandlers({
preparedGesture,
gestureConfig,
gesture,
viewTag,
webEventHandlersRef,
mountedRef,
}: AttachHandlersConfig) {
if (!preparedGesture.firstExecution) {
gestureConfig.initialize();
} else {
preparedGesture.firstExecution = false;
}
// use queueMicrotask to extract handlerTags, because all refs should be initialized
// when it's ran
ghQueueMicrotask(() => {
if (!mountedRef.current) {
return;
}
gestureConfig.prepare();
});
for (const handler of gesture) {
checkGestureCallbacksForWorklets(handler);
RNGestureHandlerModule.createGestureHandler(
handler.handlerName,
handler.handlerTag,
filterConfig(handler.config, ALLOWED_PROPS)
);
registerHandler(handler.handlerTag, handler, handler.config.testId);
}
// use queueMicrotask to extract handlerTags, because all refs should be initialized
// when it's ran
ghQueueMicrotask(() => {
if (!mountedRef.current) {
return;
}
for (const handler of gesture) {
let requireToFail: number[] = [];
if (handler.config.requireToFail) {
requireToFail = extractValidHandlerTags(handler.config.requireToFail);
}
let simultaneousWith: number[] = [];
if (handler.config.simultaneousWith) {
simultaneousWith = extractValidHandlerTags(
handler.config.simultaneousWith
);
}
let blocksHandlers: number[] = [];
if (handler.config.blocksHandlers) {
blocksHandlers = extractValidHandlerTags(handler.config.blocksHandlers);
}
RNGestureHandlerModule.updateGestureHandler(
handler.handlerTag,
filterConfig(handler.config, ALLOWED_PROPS, {
simultaneousHandlers: simultaneousWith,
waitFor: requireToFail,
blocksHandlers: blocksHandlers,
})
);
}
scheduleFlushOperations();
});
preparedGesture.config = gesture;
for (const gesture of preparedGesture.config) {
const actionType = gesture.shouldUseReanimated
? ActionType.REANIMATED_WORKLET
: ActionType.JS_FUNCTION_NEW_API;
if (Platform.OS === 'web') {
(
RNGestureHandlerModule.attachGestureHandler as typeof RNGestureHandlerModuleWeb.attachGestureHandler
)(
gesture.handlerTag,
viewTag,
ActionType.JS_FUNCTION_OLD_API, // ignored on web
webEventHandlersRef
);
} else {
RNGestureHandlerModule.attachGestureHandler(
gesture.handlerTag,
viewTag,
actionType
);
}
}
if (preparedGesture.animatedHandlers) {
const isAnimatedGesture = (g: GestureType) => g.shouldUseReanimated;
preparedGesture.animatedHandlers.value = gesture
.filter(isAnimatedGesture)
.map((g) => g.handlers) as unknown as HandlerCallbacks<
Record<string, unknown>
>[];
}
}
function updateHandlers(
preparedGesture: GestureConfigReference,
gestureConfig: ComposedGesture | GestureType,
gesture: GestureType[],
mountedRef: React.RefObject<boolean>
) {
gestureConfig.prepare();
for (let i = 0; i < gesture.length; i++) {
const handler = preparedGesture.config[i];
checkGestureCallbacksForWorklets(handler);
// only update handlerTag when it's actually different, it may be the same
// if gesture config object is wrapped with useMemo
if (gesture[i].handlerTag !== handler.handlerTag) {
gesture[i].handlerTag = handler.handlerTag;
gesture[i].handlers.handlerTag = handler.handlerTag;
}
}
// use queueMicrotask to extract handlerTags, because when it's ran, all refs should be updated
// and handlerTags in BaseGesture references should be updated in the loop above (we need to wait
// in case of external relations)
ghQueueMicrotask(() => {
if (!mountedRef.current) {
return;
}
for (let i = 0; i < gesture.length; i++) {
const handler = preparedGesture.config[i];
handler.config = gesture[i].config;
handler.handlers = gesture[i].handlers;
const requireToFail = extractValidHandlerTags(
handler.config.requireToFail
);
const simultaneousWith = extractValidHandlerTags(
handler.config.simultaneousWith
);
RNGestureHandlerModule.updateGestureHandler(
handler.handlerTag,
filterConfig(handler.config, ALLOWED_PROPS, {
simultaneousHandlers: simultaneousWith,
waitFor: requireToFail,
})
);
registerHandler(handler.handlerTag, handler, handler.config.testId);
}
if (preparedGesture.animatedHandlers) {
const previousHandlersValue =
preparedGesture.animatedHandlers.value ?? [];
const newHandlersValue = preparedGesture.config
.filter((g) => g.shouldUseReanimated) // ignore gestures that shouldn't run on UI
.map((g) => g.handlers) as unknown as HandlerCallbacks<
Record<string, unknown>
>[];
// if amount of gesture configs changes, we need to update the callbacks in shared value
let shouldUpdateSharedValue =
previousHandlersValue.length !== newHandlersValue.length;
if (!shouldUpdateSharedValue) {
// if the amount is the same, we need to check if any of the configs inside has changed
for (let i = 0; i < newHandlersValue.length; i++) {
if (
// we can use the `gestureId` prop as it's unique for every config instance
newHandlersValue[i].gestureId !== previousHandlersValue[i].gestureId
) {
shouldUpdateSharedValue = true;
break;
}
}
}
if (shouldUpdateSharedValue) {
preparedGesture.animatedHandlers.value = newHandlersValue;
}
}
scheduleFlushOperations();
});
}
function needsToReattach(
preparedGesture: GestureConfigReference,
gesture: GestureType[]
) {
if (gesture.length !== preparedGesture.config.length) {
return true;
}
for (let i = 0; i < gesture.length; i++) {
if (
gesture[i].handlerName !== preparedGesture.config[i].handlerName ||
gesture[i].shouldUseReanimated !==
preparedGesture.config[i].shouldUseReanimated
) {
return true;
}
}
return false;
}
function isStateChangeEvent(
event: GestureUpdateEvent | GestureStateChangeEvent | GestureTouchEvent
): event is GestureStateChangeEvent {
'worklet';
// @ts-ignore Yes, the oldState prop is missing on GestureTouchEvent, that's the point
return event.oldState != null;
}
function isTouchEvent(
event: GestureUpdateEvent | GestureStateChangeEvent | GestureTouchEvent
): event is GestureTouchEvent {
'worklet';
return event.eventType != null;
}
function getHandler(
type: CALLBACK_TYPE,
gesture: HandlerCallbacks<Record<string, unknown>>
) {
'worklet';
switch (type) {
case CALLBACK_TYPE.BEGAN:
return gesture.onBegin;
case CALLBACK_TYPE.START:
return gesture.onStart;
case CALLBACK_TYPE.UPDATE:
return gesture.onUpdate;
case CALLBACK_TYPE.CHANGE:
return gesture.onChange;
case CALLBACK_TYPE.END:
return gesture.onEnd;
case CALLBACK_TYPE.FINALIZE:
return gesture.onFinalize;
case CALLBACK_TYPE.TOUCHES_DOWN:
return gesture.onTouchesDown;
case CALLBACK_TYPE.TOUCHES_MOVE:
return gesture.onTouchesMove;
case CALLBACK_TYPE.TOUCHES_UP:
return gesture.onTouchesUp;
case CALLBACK_TYPE.TOUCHES_CANCELLED:
return gesture.onTouchesCancelled;
}
}
function touchEventTypeToCallbackType(
eventType: TouchEventType
): CALLBACK_TYPE {
'worklet';
switch (eventType) {
case TouchEventType.TOUCHES_DOWN:
return CALLBACK_TYPE.TOUCHES_DOWN;
case TouchEventType.TOUCHES_MOVE:
return CALLBACK_TYPE.TOUCHES_MOVE;
case TouchEventType.TOUCHES_UP:
return CALLBACK_TYPE.TOUCHES_UP;
case TouchEventType.TOUCHES_CANCELLED:
return CALLBACK_TYPE.TOUCHES_CANCELLED;
}
return CALLBACK_TYPE.UNDEFINED;
}
function runWorklet(
type: CALLBACK_TYPE,
gesture: HandlerCallbacks<Record<string, unknown>>,
event: GestureStateChangeEvent | GestureUpdateEvent | GestureTouchEvent,
...args: any[]
) {
'worklet';
const handler = getHandler(type, gesture);
if (gesture.isWorklet[type]) {
// @ts-ignore Logic below makes sure the correct event is send to the
// correct handler.
handler?.(event, ...args);
} else if (handler) {
console.warn(tagMessage('Animated gesture callback must be a worklet'));
}
}
function useAnimatedGesture(
preparedGesture: GestureConfigReference,
needsRebuild: boolean
) {
if (!Reanimated) {
return;
}
// Hooks are called conditionally, but the condition is whether the
// react-native-reanimated is installed, which shouldn't change while running
// eslint-disable-next-line react-hooks/rules-of-hooks
const sharedHandlersCallbacks = Reanimated.useSharedValue<
HandlerCallbacks<Record<string, unknown>>[] | null
>(null);
// eslint-disable-next-line react-hooks/rules-of-hooks
const lastUpdateEvent = Reanimated.useSharedValue<
(GestureUpdateEvent | undefined)[]
>([]);
// not every gesture needs a state controller, init them lazily
const stateControllers: GestureStateManagerType[] = [];
const callback = (
event: GestureStateChangeEvent | GestureUpdateEvent | GestureTouchEvent
) => {
'worklet';
const currentCallback = sharedHandlersCallbacks.value;
if (!currentCallback) {
return;
}
for (let i = 0; i < currentCallback.length; i++) {
const gesture = currentCallback[i];
if (event.handlerTag === gesture.handlerTag) {
if (isStateChangeEvent(event)) {
if (
event.oldState === State.UNDETERMINED &&
event.state === State.BEGAN
) {
runWorklet(CALLBACK_TYPE.BEGAN, gesture, event);
} else if (
(event.oldState === State.BEGAN ||
event.oldState === State.UNDETERMINED) &&
event.state === State.ACTIVE
) {
runWorklet(CALLBACK_TYPE.START, gesture, event);
lastUpdateEvent.value[gesture.handlerTag] = undefined;
} else if (
event.oldState !== event.state &&
event.state === State.END
) {
if (event.oldState === State.ACTIVE) {
runWorklet(CALLBACK_TYPE.END, gesture, event, true);
}
runWorklet(CALLBACK_TYPE.FINALIZE, gesture, event, true);
} else if (
(event.state === State.FAILED || event.state === State.CANCELLED) &&
event.state !== event.oldState
) {
if (event.oldState === State.ACTIVE) {
runWorklet(CALLBACK_TYPE.END, gesture, event, false);
}
runWorklet(CALLBACK_TYPE.FINALIZE, gesture, event, false);
}
} else if (isTouchEvent(event)) {
if (!stateControllers[i]) {
stateControllers[i] = GestureStateManager.create(event.handlerTag);
}
if (event.eventType !== TouchEventType.UNDETERMINED) {
runWorklet(
touchEventTypeToCallbackType(event.eventType),
gesture,
event,
stateControllers[i]
);
}
} else {
runWorklet(CALLBACK_TYPE.UPDATE, gesture, event);
if (gesture.onChange && gesture.changeEventCalculator) {
runWorklet(
CALLBACK_TYPE.CHANGE,
gesture,
gesture.changeEventCalculator?.(
event,
lastUpdateEvent.value[gesture.handlerTag]
)
);
lastUpdateEvent.value[gesture.handlerTag] = event;
}
}
}
}
};
// eslint-disable-next-line react-hooks/rules-of-hooks
const event = Reanimated.useEvent(
callback,
['onGestureHandlerStateChange', 'onGestureHandlerEvent'],
needsRebuild
);
preparedGesture.animatedEventHandler = event;
preparedGesture.animatedHandlers = sharedHandlersCallbacks;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function validateDetectorChildren(ref: any) {
// finds the first native view under the Wrap component and traverses the fiber tree upwards
// to check whether there is more than one native view as a pseudo-direct child of GestureDetector
// i.e. this is not ok:
// Wrap
// |
// / \
// / \
// / \
// / \
// NativeView NativeView
//
// but this is fine:
// Wrap
// |
// NativeView
// |
// / \
// / \
// / \
// / \
// NativeView NativeView
if (__DEV__ && Platform.OS !== 'web') {
const REACT_NATIVE_VERSION = getReactNativeVersion();
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const wrapType =
REACT_NATIVE_VERSION.minor > 63 || REACT_NATIVE_VERSION.major > 0
? // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
ref._reactInternals.elementType
: // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
ref._reactInternalFiber.elementType;
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
let instance =
RNRenderer.findHostInstance_DEPRECATED(
ref
)._internalFiberInstanceHandleDEV;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
while (instance && instance.elementType !== wrapType) {
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (instance.sibling) {
throw new Error(
'GestureDetector has more than one native view as its children. This can happen if you are using a custom component that renders multiple views, like React.Fragment. You should wrap content of GestureDetector with a <View> or <Animated.View>.'
);
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access
instance = instance.return;
}
}
}
const applyUserSelectProp = (
userSelect: UserSelect,
gesture: ComposedGesture | GestureType
): void => {
for (const g of gesture.toGestureArray()) {
g.config.userSelect = userSelect;
}
};
const applyEnableContextMenuProp = (
enableContextMenu: boolean,
gesture: ComposedGesture | GestureType
): void => {
for (const g of gesture.toGestureArray()) {
g.config.enableContextMenu = enableContextMenu;
}
};
const applyTouchActionProp = (
touchAction: TouchAction,
gesture: ComposedGesture | GestureType
): void => {
for (const g of gesture.toGestureArray()) {
g.config.touchAction = touchAction;
}
};
interface GestureDetectorProps {
/**
* A gesture object containing the configuration and callbacks.
* Can be any of:
* - base gestures (`Tap`, `Pan`, ...)
* - `ComposedGesture` (`Race`, `Simultaneous`, `Exclusive`)
*/
gesture: ComposedGesture | GestureType;
children?: React.ReactNode;
/**
* #### Web only
* This parameter allows to specify which `userSelect` property should be applied to underlying view.
* Possible values are `"none" | "auto" | "text"`. Default value is set to `"none"`.
*/
userSelect?: UserSelect;
/**
* #### Web only
* Specifies whether context menu should be enabled after clicking on underlying view with right mouse button.
* Default value is set to `false`.
*/
enableContextMenu?: boolean;
/**
* #### Web only
* This parameter allows to specify which `touchAction` property should be applied to underlying view.
* Supports all CSS touch-action values (e.g. `"none"`, `"pan-y"`). Default value is set to `"none"`.
*/
touchAction?: TouchAction;
}
interface GestureDetectorState {
firstRender: boolean;
viewRef: React.Component | null;
previousViewTag: number;
forceReattach: boolean;
}
/**
* `GestureDetector` is responsible for creating and updating native gesture handlers based on the config of provided gesture.
*
* ### Props
* - `gesture`
* - `userSelect` (**Web only**)
* - `enableContextMenu` (**Web only**)
* - `touchAction` (**Web only**)
*
* ### Remarks
* - Gesture Detector will use first native view in its subtree to recognize gestures, however if this view is used only to group its children it may get automatically collapsed.
* - Using the same instance of a gesture across multiple Gesture Detectors is not possible.
*
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/gesture-detector
*/
export const GestureDetector = (props: GestureDetectorProps) => {
const rootViewContext = useContext(GestureHandlerRootViewContext);
if (__DEV__ && !rootViewContext && !isJestEnv() && Platform.OS !== 'web') {
throw new Error(
'GestureDetector must be used as a descendant of GestureHandlerRootView. Otherwise the gestures will not be recognized. See https://docs.swmansion.com/react-native-gesture-handler/docs/installation for more details.'
);
}
const gestureConfig = props.gesture;
if (props.userSelect) {
applyUserSelectProp(props.userSelect, gestureConfig);
}
if (props.enableContextMenu !== undefined) {
applyEnableContextMenuProp(props.enableContextMenu, gestureConfig);
}
if (props.touchAction !== undefined) {
applyTouchActionProp(props.touchAction, gestureConfig);
}
const gesture = gestureConfig.toGestureArray();
const useReanimatedHook = gesture.some((g) => g.shouldUseReanimated);
// store state in ref to prevent unnecessary renders
const state = useRef<GestureDetectorState>({
firstRender: true,
viewRef: null,
previousViewTag: -1,
forceReattach: false,
}).current;
const mountedRef = useRef(false);
const webEventHandlersRef = useRef<WebEventHandler>({
onGestureHandlerEvent: (e: HandlerStateChangeEvent<unknown>) => {
onGestureHandlerEvent(e.nativeEvent);
},
onGestureHandlerStateChange: isNewWebImplementationEnabled()
? (e: HandlerStateChangeEvent<unknown>) => {
onGestureHandlerEvent(e.nativeEvent);
}
: undefined,
});
const [renderState, setRenderState] = useState(false);
function forceRender() {
setRenderState(!renderState);
}
const preparedGesture = React.useRef<GestureConfigReference>({
config: gesture,
animatedEventHandler: null,
animatedHandlers: null,
firstExecution: true,
useReanimatedHook: useReanimatedHook,
}).current;
if (useReanimatedHook !== preparedGesture.useReanimatedHook) {
throw new Error(
tagMessage(
'You cannot change the thread the callbacks are ran on while the app is running'
)
);
}
function onHandlersUpdate(skipConfigUpdate?: boolean) {
// if the underlying view has changed we need to reattach handlers to the new view
const viewTag = findNodeHandle(state.viewRef) as number;
const forceReattach = viewTag !== state.previousViewTag;
if (forceReattach || needsToReattach(preparedGesture, gesture)) {
validateDetectorChildren(state.viewRef);
dropHandlers(preparedGesture);
attachHandlers({
preparedGesture,
gestureConfig,
gesture,
webEventHandlersRef,
viewTag,
mountedRef,
});
state.previousViewTag = viewTag;
state.forceReattach = forceReattach;
if (forceReattach) {
forceRender();
}
} else if (!skipConfigUpdate) {
updateHandlers(preparedGesture, gestureConfig, gesture, mountedRef);
}
}
// Reanimated event should be rebuilt only when gestures are reattached, otherwise
// config update will be enough as all necessary items are stored in shared values anyway
const needsToRebuildReanimatedEvent =
preparedGesture.firstExecution ||
needsToReattach(preparedGesture, gesture) ||
state.forceReattach;
state.forceReattach = false;
if (preparedGesture.firstExecution) {
gestureConfig.initialize();
}
if (useReanimatedHook) {
// Whether animatedGesture or gesture is used shouldn't change while the app is running
// eslint-disable-next-line react-hooks/rules-of-hooks
useAnimatedGesture(preparedGesture, needsToRebuildReanimatedEvent);
}
useEffect(() => {
const viewTag = findNodeHandle(state.viewRef) as number;
state.firstRender = true;
mountedRef.current = true;
validateDetectorChildren(state.viewRef);
attachHandlers({
preparedGesture,
gestureConfig,
gesture,
webEventHandlersRef,
viewTag,
mountedRef,
});
return () => {
mountedRef.current = false;
dropHandlers(preparedGesture);
};
}, []);
useEffect(() => {
if (!state.firstRender) {
onHandlersUpdate();
} else {
state.firstRender = false;
}
}, [props]);
const refFunction = (ref: unknown) => {
if (ref !== null) {
// @ts-ignore Just setting the view ref
state.viewRef = ref;
// if it's the first render, also set the previousViewTag to prevent reattaching gestures when not needed
if (state.previousViewTag === -1) {
state.previousViewTag = findNodeHandle(state.viewRef) as number;
}
// pass true as `skipConfigUpdate`, here we only want to trigger the eventual reattaching of handlers
// in case the view has changed, while config update would be handled be the `useEffect` above
onHandlersUpdate(true);
if (isFabric() && global.isFormsStackingContext) {
const node = getShadowNodeFromRef(ref);
if (global.isFormsStackingContext(node) === false) {
console.error(
tagMessage(
'GestureDetector has received a child that may get view-flattened. ' +
'\nTo prevent it from misbehaving you need to wrap the child with a `<View collapsable={false}>`.'
)
);
}
}
}
};
if (useReanimatedHook) {
return (
<AnimatedWrap
ref={refFunction}
onGestureHandlerEvent={preparedGesture.animatedEventHandler}>
{props.children}
</AnimatedWrap>
);
} else {
return <Wrap ref={refFunction}>{props.children}</Wrap>;
}
};
class Wrap extends React.Component<{
onGestureHandlerEvent?: unknown;
// implicit `children` prop has been removed in @types/react^18.0.0
children?: React.ReactNode;
}> {
render() {
try {
// I don't think that fighting with types over such a simple function is worth it
// The only thing it does is add 'collapsable: false' to the child component
// to make sure it is in the native view hierarchy so the detector can find
// correct viewTag to attach to.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const child: any = React.Children.only(this.props.children);
return React.cloneElement(
child,
{ collapsable: false },
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
child.props.children
);
} catch (e) {
throw new Error(
tagMessage(
`GestureDetector got more than one view as a child. If you want the gesture to work on multiple views, wrap them with a common parent and attach the gesture to that view.`
)
);
}
}
}
const AnimatedWrap = Reanimated?.default?.createAnimatedComponent(Wrap) ?? Wrap;

View File

@@ -0,0 +1,155 @@
import { DeviceEventEmitter, EmitterSubscription } from 'react-native';
import { State } from '../../State';
import { TouchEventType } from '../../TouchEventType';
import {
GestureTouchEvent,
GestureUpdateEvent,
GestureStateChangeEvent,
} from '../gestureHandlerCommon';
import { findHandler, findOldGestureHandler } from '../handlersRegistry';
import { BaseGesture } from './gesture';
import {
GestureStateManager,
GestureStateManagerType,
} from './gestureStateManager';
let gestureHandlerEventSubscription: EmitterSubscription | null = null;
let gestureHandlerStateChangeEventSubscription: EmitterSubscription | null =
null;
const gestureStateManagers: Map<number, GestureStateManagerType> = new Map<
number,
GestureStateManagerType
>();
const lastUpdateEvent: (GestureUpdateEvent | undefined)[] = [];
function isStateChangeEvent(
event: GestureUpdateEvent | GestureStateChangeEvent | GestureTouchEvent
): event is GestureStateChangeEvent {
// @ts-ignore oldState doesn't exist on GestureTouchEvent and that's the point
return event.oldState != null;
}
function isTouchEvent(
event: GestureUpdateEvent | GestureStateChangeEvent | GestureTouchEvent
): event is GestureTouchEvent {
return event.eventType != null;
}
export function onGestureHandlerEvent(
event: GestureUpdateEvent | GestureStateChangeEvent | GestureTouchEvent
) {
const handler = findHandler(event.handlerTag) as BaseGesture<
Record<string, unknown>
>;
if (handler) {
if (isStateChangeEvent(event)) {
if (
event.oldState === State.UNDETERMINED &&
event.state === State.BEGAN
) {
handler.handlers.onBegin?.(event);
} else if (
(event.oldState === State.BEGAN ||
event.oldState === State.UNDETERMINED) &&
event.state === State.ACTIVE
) {
handler.handlers.onStart?.(event);
lastUpdateEvent[handler.handlers.handlerTag] = event;
} else if (event.oldState !== event.state && event.state === State.END) {
if (event.oldState === State.ACTIVE) {
handler.handlers.onEnd?.(event, true);
}
handler.handlers.onFinalize?.(event, true);
lastUpdateEvent[handler.handlers.handlerTag] = undefined;
} else if (
(event.state === State.FAILED || event.state === State.CANCELLED) &&
event.oldState !== event.state
) {
if (event.oldState === State.ACTIVE) {
handler.handlers.onEnd?.(event, false);
}
handler.handlers.onFinalize?.(event, false);
gestureStateManagers.delete(event.handlerTag);
lastUpdateEvent[handler.handlers.handlerTag] = undefined;
}
} else if (isTouchEvent(event)) {
if (!gestureStateManagers.has(event.handlerTag)) {
gestureStateManagers.set(
event.handlerTag,
GestureStateManager.create(event.handlerTag)
);
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const manager = gestureStateManagers.get(event.handlerTag)!;
switch (event.eventType) {
case TouchEventType.TOUCHES_DOWN:
handler.handlers?.onTouchesDown?.(event, manager);
break;
case TouchEventType.TOUCHES_MOVE:
handler.handlers?.onTouchesMove?.(event, manager);
break;
case TouchEventType.TOUCHES_UP:
handler.handlers?.onTouchesUp?.(event, manager);
break;
case TouchEventType.TOUCHES_CANCELLED:
handler.handlers?.onTouchesCancelled?.(event, manager);
break;
}
} else {
handler.handlers.onUpdate?.(event);
if (handler.handlers.onChange && handler.handlers.changeEventCalculator) {
handler.handlers.onChange?.(
handler.handlers.changeEventCalculator?.(
event,
lastUpdateEvent[handler.handlers.handlerTag]
)
);
lastUpdateEvent[handler.handlers.handlerTag] = event;
}
}
} else {
const oldHandler = findOldGestureHandler(event.handlerTag);
if (oldHandler) {
const nativeEvent = { nativeEvent: event };
if (isStateChangeEvent(event)) {
oldHandler.onGestureStateChange(nativeEvent);
} else {
oldHandler.onGestureEvent(nativeEvent);
}
return;
}
}
}
export function startListening() {
stopListening();
gestureHandlerEventSubscription = DeviceEventEmitter.addListener(
'onGestureHandlerEvent',
onGestureHandlerEvent
);
gestureHandlerStateChangeEventSubscription = DeviceEventEmitter.addListener(
'onGestureHandlerStateChange',
onGestureHandlerEvent
);
}
export function stopListening() {
if (gestureHandlerEventSubscription) {
gestureHandlerEventSubscription.remove();
gestureHandlerEventSubscription = null;
}
if (gestureHandlerStateChangeEventSubscription) {
gestureHandlerStateChangeEventSubscription.remove();
gestureHandlerStateChangeEventSubscription = null;
}
}

View File

@@ -0,0 +1,38 @@
import { BaseGesture, BaseGestureConfig } from './gesture';
import {
FlingGestureConfig,
FlingGestureHandlerEventPayload,
} from '../FlingGestureHandler';
export class FlingGesture extends BaseGesture<FlingGestureHandlerEventPayload> {
public config: BaseGestureConfig & FlingGestureConfig = {};
constructor() {
super();
this.handlerName = 'FlingGestureHandler';
}
/**
* Determine exact number of points required to handle the fling gesture.
* @param pointers
*/
numberOfPointers(pointers: number) {
this.config.numberOfPointers = pointers;
return this;
}
/**
* Expressed allowed direction of movement.
* Expected values are exported as constants in the Directions object.
* Arguments can be combined using `|` operator. Default value is set to `MouseButton.LEFT`.
* @param direction
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/fling-gesture/#directionvalue-directions
*/
direction(direction: number) {
this.config.direction = direction;
return this;
}
}
export type FlingGestureType = InstanceType<typeof FlingGesture>;

View File

@@ -0,0 +1,88 @@
import { BaseGestureConfig, ContinousBaseGesture } from './gesture';
import {
ForceTouchGestureConfig,
ForceTouchGestureHandlerEventPayload,
} from '../ForceTouchGestureHandler';
import { GestureUpdateEvent } from '../gestureHandlerCommon';
export type ForceTouchGestureChangeEventPayload = {
forceChange: number;
};
function changeEventCalculator(
current: GestureUpdateEvent<ForceTouchGestureHandlerEventPayload>,
previous?: GestureUpdateEvent<ForceTouchGestureHandlerEventPayload>
) {
'worklet';
let changePayload: ForceTouchGestureChangeEventPayload;
if (previous === undefined) {
changePayload = {
forceChange: current.force,
};
} else {
changePayload = {
forceChange: current.force - previous.force,
};
}
return { ...current, ...changePayload };
}
export class ForceTouchGesture extends ContinousBaseGesture<
ForceTouchGestureHandlerEventPayload,
ForceTouchGestureChangeEventPayload
> {
public config: BaseGestureConfig & ForceTouchGestureConfig = {};
constructor() {
super();
this.handlerName = 'ForceTouchGestureHandler';
}
/**
* A minimal pressure that is required before gesture can activate.
* Should be a value from range [0.0, 1.0]. Default is 0.2.
* @param force
*/
minForce(force: number) {
this.config.minForce = force;
return this;
}
/**
* A maximal pressure that could be applied for gesture.
* If the pressure is greater, gesture fails. Should be a value from range [0.0, 1.0].
* @param force
*/
maxForce(force: number) {
this.config.maxForce = force;
return this;
}
/**
* Value defining if haptic feedback has to be performed on activation.
* @param value
*/
feedbackOnActivation(value: boolean) {
this.config.feedbackOnActivation = value;
return this;
}
onChange(
callback: (
event: GestureUpdateEvent<
GestureUpdateEvent<
ForceTouchGestureHandlerEventPayload &
ForceTouchGestureChangeEventPayload
>
>
) => void
) {
// @ts-ignore TS being overprotective, ForceTouchGestureHandlerEventPayload is Record
this.handlers.changeEventCalculator = changeEventCalculator;
return super.onChange(callback);
}
}
export type ForceTouchGestureType = InstanceType<typeof ForceTouchGesture>;

View File

@@ -0,0 +1,468 @@
import { FlingGestureHandlerEventPayload } from '../FlingGestureHandler';
import { ForceTouchGestureHandlerEventPayload } from '../ForceTouchGestureHandler';
import {
HitSlop,
CommonGestureConfig,
GestureTouchEvent,
GestureStateChangeEvent,
GestureUpdateEvent,
ActiveCursor,
MouseButton,
} from '../gestureHandlerCommon';
import { getNextHandlerTag } from '../handlersRegistry';
import { GestureStateManagerType } from './gestureStateManager';
import { LongPressGestureHandlerEventPayload } from '../LongPressGestureHandler';
import { PanGestureHandlerEventPayload } from '../PanGestureHandler';
import { PinchGestureHandlerEventPayload } from '../PinchGestureHandler';
import { RotationGestureHandlerEventPayload } from '../RotationGestureHandler';
import { TapGestureHandlerEventPayload } from '../TapGestureHandler';
import { NativeViewGestureHandlerPayload } from '../NativeViewGestureHandler';
import { isRemoteDebuggingEnabled } from '../../utils';
export type GestureType =
| BaseGesture<Record<string, unknown>>
| BaseGesture<Record<string, never>>
| BaseGesture<TapGestureHandlerEventPayload>
| BaseGesture<PanGestureHandlerEventPayload>
| BaseGesture<LongPressGestureHandlerEventPayload>
| BaseGesture<RotationGestureHandlerEventPayload>
| BaseGesture<PinchGestureHandlerEventPayload>
| BaseGesture<FlingGestureHandlerEventPayload>
| BaseGesture<ForceTouchGestureHandlerEventPayload>
| BaseGesture<NativeViewGestureHandlerPayload>;
export type GestureRef =
| number
| GestureType
| React.RefObject<GestureType | undefined>
| React.RefObject<React.ComponentType | undefined>; // allow adding a ref to a gesture handler
export interface BaseGestureConfig
extends CommonGestureConfig,
Record<string, unknown> {
ref?: React.MutableRefObject<GestureType | undefined>;
requireToFail?: GestureRef[];
simultaneousWith?: GestureRef[];
blocksHandlers?: GestureRef[];
needsPointerData?: boolean;
manualActivation?: boolean;
runOnJS?: boolean;
testId?: string;
cancelsTouchesInView?: boolean;
}
type TouchEventHandlerType = (
event: GestureTouchEvent,
stateManager: GestureStateManagerType
) => void;
export type HandlerCallbacks<EventPayloadT extends Record<string, unknown>> = {
gestureId: number;
handlerTag: number;
onBegin?: (event: GestureStateChangeEvent<EventPayloadT>) => void;
onStart?: (event: GestureStateChangeEvent<EventPayloadT>) => void;
onEnd?: (
event: GestureStateChangeEvent<EventPayloadT>,
success: boolean
) => void;
onFinalize?: (
event: GestureStateChangeEvent<EventPayloadT>,
success: boolean
) => void;
onUpdate?: (event: GestureUpdateEvent<EventPayloadT>) => void;
onChange?: (event: any) => void;
onTouchesDown?: TouchEventHandlerType;
onTouchesMove?: TouchEventHandlerType;
onTouchesUp?: TouchEventHandlerType;
onTouchesCancelled?: TouchEventHandlerType;
changeEventCalculator?: (
current: GestureUpdateEvent<Record<string, unknown>>,
previous?: GestureUpdateEvent<Record<string, unknown>>
) => GestureUpdateEvent<Record<string, unknown>>;
isWorklet: boolean[];
};
export const CALLBACK_TYPE = {
UNDEFINED: 0,
BEGAN: 1,
START: 2,
UPDATE: 3,
CHANGE: 4,
END: 5,
FINALIZE: 6,
TOUCHES_DOWN: 7,
TOUCHES_MOVE: 8,
TOUCHES_UP: 9,
TOUCHES_CANCELLED: 10,
} as const;
// Allow using CALLBACK_TYPE as object and type
// eslint-disable-next-line @typescript-eslint/no-redeclare
export type CALLBACK_TYPE = typeof CALLBACK_TYPE[keyof typeof CALLBACK_TYPE];
export abstract class Gesture {
/**
* Return array of gestures, providing the same interface for creating and updating
* handlers, no matter which object was used to create gesture instance.
*/
abstract toGestureArray(): GestureType[];
/**
* Assign handlerTag to the gesture instance and set ref.current (if a ref is set)
*/
abstract initialize(): void;
/**
* Make sure that values of properties defining relations are arrays. Do any necessary
* preprocessing required to configure relations between handlers. Called just before
* updating the handler on the native side.
*/
abstract prepare(): void;
}
let nextGestureId = 0;
export abstract class BaseGesture<
EventPayloadT extends Record<string, unknown>
> extends Gesture {
private gestureId = -1;
public handlerTag = -1;
public handlerName = '';
public config: BaseGestureConfig = {};
public handlers: HandlerCallbacks<EventPayloadT> = {
gestureId: -1,
handlerTag: -1,
isWorklet: [],
};
constructor() {
super();
// Used to check whether the gesture config has been updated when wrapping it
// with `useMemo`. Since every config will have a unique id, when the dependencies
// don't change, the config won't be recreated and the id will stay the same.
// If the id is different, it means that the config has changed and the gesture
// needs to be updated.
this.gestureId = nextGestureId++;
this.handlers.gestureId = this.gestureId;
}
private addDependency(
key: 'simultaneousWith' | 'requireToFail' | 'blocksHandlers',
gesture: Exclude<GestureRef, number>
) {
const value = this.config[key];
this.config[key] = value
? Array<GestureRef>().concat(value, gesture)
: [gesture];
}
/**
* Sets a `ref` to the gesture object, allowing for interoperability with the old API.
* @param ref
*/
withRef(ref: React.MutableRefObject<GestureType | undefined>) {
this.config.ref = ref;
return this;
}
// eslint-disable-next-line @typescript-eslint/ban-types
protected isWorklet(callback: Function) {
//@ts-ignore if callback is a worklet, the property will be available, if not then the check will return false
return callback.__workletHash !== undefined;
}
/**
* Set the callback that is being called when given gesture handler starts receiving touches.
* At the moment of this callback the handler is in `BEGAN` state and we don't know yet if it will recognize the gesture at all.
* @param callback
*/
onBegin(callback: (event: GestureStateChangeEvent<EventPayloadT>) => void) {
this.handlers.onBegin = callback;
this.handlers.isWorklet[CALLBACK_TYPE.BEGAN] = this.isWorklet(callback);
return this;
}
/**
* Set the callback that is being called when the gesture is recognized by the handler and it transitions to the `ACTIVE` state.
* @param callback
*/
onStart(callback: (event: GestureStateChangeEvent<EventPayloadT>) => void) {
this.handlers.onStart = callback;
this.handlers.isWorklet[CALLBACK_TYPE.START] = this.isWorklet(callback);
return this;
}
/**
* Set the callback that is being called when the gesture that was recognized by the handler finishes and handler reaches `END` state.
* It will be called only if the handler was previously in the `ACTIVE` state.
* @param callback
*/
onEnd(
callback: (
event: GestureStateChangeEvent<EventPayloadT>,
success: boolean
) => void
) {
this.handlers.onEnd = callback;
//@ts-ignore if callback is a worklet, the property will be available, if not then the check will return false
this.handlers.isWorklet[CALLBACK_TYPE.END] = this.isWorklet(callback);
return this;
}
/**
* Set the callback that is being called when the handler finalizes handling gesture - the gesture was recognized and has finished or it failed to recognize.
* @param callback
*/
onFinalize(
callback: (
event: GestureStateChangeEvent<EventPayloadT>,
success: boolean
) => void
) {
this.handlers.onFinalize = callback;
//@ts-ignore if callback is a worklet, the property will be available, if not then the check will return false
this.handlers.isWorklet[CALLBACK_TYPE.FINALIZE] = this.isWorklet(callback);
return this;
}
/**
* Set the `onTouchesDown` callback which is called every time a pointer is placed on the screen.
* @param callback
*/
onTouchesDown(callback: TouchEventHandlerType) {
this.config.needsPointerData = true;
this.handlers.onTouchesDown = callback;
this.handlers.isWorklet[CALLBACK_TYPE.TOUCHES_DOWN] =
this.isWorklet(callback);
return this;
}
/**
* Set the `onTouchesMove` callback which is called every time a pointer is moved on the screen.
* @param callback
*/
onTouchesMove(callback: TouchEventHandlerType) {
this.config.needsPointerData = true;
this.handlers.onTouchesMove = callback;
this.handlers.isWorklet[CALLBACK_TYPE.TOUCHES_MOVE] =
this.isWorklet(callback);
return this;
}
/**
* Set the `onTouchesUp` callback which is called every time a pointer is lifted from the screen.
* @param callback
*/
onTouchesUp(callback: TouchEventHandlerType) {
this.config.needsPointerData = true;
this.handlers.onTouchesUp = callback;
this.handlers.isWorklet[CALLBACK_TYPE.TOUCHES_UP] =
this.isWorklet(callback);
return this;
}
/**
* Set the `onTouchesCancelled` callback which is called every time a pointer stops being tracked, for example when the gesture finishes.
* @param callback
*/
onTouchesCancelled(callback: TouchEventHandlerType) {
this.config.needsPointerData = true;
this.handlers.onTouchesCancelled = callback;
this.handlers.isWorklet[CALLBACK_TYPE.TOUCHES_CANCELLED] =
this.isWorklet(callback);
return this;
}
/**
* Indicates whether the given handler should be analyzing stream of touch events or not.
* @param enabled
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/pan-gesture#enabledvalue-boolean
*/
enabled(enabled: boolean) {
this.config.enabled = enabled;
return this;
}
/**
* When true the handler will cancel or fail recognition (depending on its current state) whenever the finger leaves the area of the connected view.
* @param value
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/pan-gesture#shouldcancelwhenoutsidevalue-boolean
*/
shouldCancelWhenOutside(value: boolean) {
this.config.shouldCancelWhenOutside = value;
return this;
}
/**
* This parameter enables control over what part of the connected view area can be used to begin recognizing the gesture.
* When a negative number is provided the bounds of the view will reduce the area by the given number of points in each of the sides evenly.
* @param hitSlop
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/pan-gesture#hitslopsettings
*/
hitSlop(hitSlop: HitSlop) {
this.config.hitSlop = hitSlop;
return this;
}
/**
* #### Web only
* This parameter allows to specify which `cursor` should be used when gesture activates.
* Supports all CSS cursor values (e.g. `"grab"`, `"zoom-in"`). Default value is set to `"auto"`.
* @param activeCursor
*/
activeCursor(activeCursor: ActiveCursor) {
this.config.activeCursor = activeCursor;
return this;
}
/**
* #### Web & Android only
* Allows users to choose which mouse button should handler respond to.
* Arguments can be combined using `|` operator, e.g. `mouseButton(MouseButton.LEFT | MouseButton.RIGHT)`.
* Default value is set to `MouseButton.LEFT`.
* @param mouseButton
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/pan-gesture#mousebuttonvalue-mousebutton-web--android-only
*/
mouseButton(mouseButton: MouseButton) {
this.config.mouseButton = mouseButton;
return this;
}
/**
* When `react-native-reanimated` is installed, the callbacks passed to the gestures are automatically workletized and run on the UI thread when called.
* This option allows for changing this behavior: when `true`, all the callbacks will be run on the JS thread instead of the UI thread, regardless of whether they are worklets or not.
* Defaults to `false`.
* @param runOnJS
*/
runOnJS(runOnJS: boolean) {
this.config.runOnJS = runOnJS;
return this;
}
/**
* Allows gestures across different components to be recognized simultaneously.
* @param gestures
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/gesture-composition/#simultaneouswithexternalgesture
*/
simultaneousWithExternalGesture(...gestures: Exclude<GestureRef, number>[]) {
for (const gesture of gestures) {
this.addDependency('simultaneousWith', gesture);
}
return this;
}
/**
* Allows to delay activation of the handler until all handlers passed as arguments to this method fail (or don't begin at all).
* @param gestures
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/gesture-composition/#requireexternalgesturetofail
*/
requireExternalGestureToFail(...gestures: Exclude<GestureRef, number>[]) {
for (const gesture of gestures) {
this.addDependency('requireToFail', gesture);
}
return this;
}
/**
* Works similarily to `requireExternalGestureToFail` but the direction of the relation is reversed - instead of being one-to-many relation, it's many-to-one.
* @param gestures
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/gesture-composition/#blocksexternalgesture
*/
blocksExternalGesture(...gestures: Exclude<GestureRef, number>[]) {
for (const gesture of gestures) {
this.addDependency('blocksHandlers', gesture);
}
return this;
}
/**
* Sets a `testID` property for gesture object, allowing for querying for it in tests.
* @param id
*/
withTestId(id: string) {
this.config.testId = id;
return this;
}
/**
* #### iOS only
* When `true`, the handler will cancel touches for native UI components (`UIButton`, `UISwitch`, etc) it's attached to when it becomes `ACTIVE`.
* Default value is `true`.
* @param value
*/
cancelsTouchesInView(value: boolean) {
this.config.cancelsTouchesInView = value;
return this;
}
initialize() {
this.handlerTag = getNextHandlerTag();
this.handlers = { ...this.handlers, handlerTag: this.handlerTag };
if (this.config.ref) {
this.config.ref.current = this as GestureType;
}
}
toGestureArray(): GestureType[] {
return [this as GestureType];
}
// eslint-disable-next-line @typescript-eslint/no-empty-function
prepare() {}
get shouldUseReanimated(): boolean {
// use Reanimated when runOnJS isn't set explicitly,
// and all defined callbacks are worklets,
// and remote debugging is disabled
return (
this.config.runOnJS !== true &&
!this.handlers.isWorklet.includes(false) &&
!isRemoteDebuggingEnabled()
);
}
}
export abstract class ContinousBaseGesture<
EventPayloadT extends Record<string, unknown>,
EventChangePayloadT extends Record<string, unknown>
> extends BaseGesture<EventPayloadT> {
/**
* Set the callback that is being called every time the gesture receives an update while it's active.
* @param callback
*/
onUpdate(callback: (event: GestureUpdateEvent<EventPayloadT>) => void) {
this.handlers.onUpdate = callback;
this.handlers.isWorklet[CALLBACK_TYPE.UPDATE] = this.isWorklet(callback);
return this;
}
/**
* Set the callback that is being called every time the gesture receives an update while it's active.
* This callback will receive information about change in value in relation to the last received event.
* @param callback
*/
onChange(
callback: (
event: GestureUpdateEvent<EventPayloadT & EventChangePayloadT>
) => void
) {
this.handlers.onChange = callback;
this.handlers.isWorklet[CALLBACK_TYPE.CHANGE] = this.isWorklet(callback);
return this;
}
/**
* When `true` the handler will not activate by itself even if its activation criteria are met.
* Instead you can manipulate its state using state manager.
* @param manualActivation
*/
manualActivation(manualActivation: boolean) {
this.config.manualActivation = manualActivation;
return this;
}
}

View File

@@ -0,0 +1,122 @@
import { BaseGesture, Gesture, GestureRef, GestureType } from './gesture';
function extendRelation(
currentRelation: GestureRef[] | undefined,
extendWith: GestureType[]
) {
if (currentRelation === undefined) {
return [...extendWith];
} else {
return [...currentRelation, ...extendWith];
}
}
export class ComposedGesture extends Gesture {
protected gestures: Gesture[] = [];
protected simultaneousGestures: GestureType[] = [];
protected requireGesturesToFail: GestureType[] = [];
constructor(...gestures: Gesture[]) {
super();
this.gestures = gestures;
}
protected prepareSingleGesture(
gesture: Gesture,
simultaneousGestures: GestureType[],
requireGesturesToFail: GestureType[]
) {
if (gesture instanceof BaseGesture) {
const newConfig = { ...gesture.config };
newConfig.simultaneousWith = extendRelation(
newConfig.simultaneousWith,
simultaneousGestures
);
newConfig.requireToFail = extendRelation(
newConfig.requireToFail,
requireGesturesToFail
);
gesture.config = newConfig;
} else if (gesture instanceof ComposedGesture) {
gesture.simultaneousGestures = simultaneousGestures;
gesture.requireGesturesToFail = requireGesturesToFail;
gesture.prepare();
}
}
prepare() {
for (const gesture of this.gestures) {
this.prepareSingleGesture(
gesture,
this.simultaneousGestures,
this.requireGesturesToFail
);
}
}
initialize() {
for (const gesture of this.gestures) {
gesture.initialize();
}
}
toGestureArray(): GestureType[] {
return this.gestures.flatMap((gesture) => gesture.toGestureArray());
}
}
export class SimultaneousGesture extends ComposedGesture {
prepare() {
// this piece of magic works something like this:
// for every gesture in the array
const simultaneousArrays = this.gestures.map((gesture) =>
// we take the array it's in
this.gestures
// and make a copy without it
.filter((x) => x !== gesture)
// then we flatmap the result to get list of raw (not composed) gestures
// this way we don't make the gestures simultaneous with themselves, which is
// important when the gesture is `ExclusiveGesture` - we don't want to make
// exclusive gestures simultaneous
.flatMap((x) => x.toGestureArray())
);
for (let i = 0; i < this.gestures.length; i++) {
this.prepareSingleGesture(
this.gestures[i],
simultaneousArrays[i],
this.requireGesturesToFail
);
}
}
}
export class ExclusiveGesture extends ComposedGesture {
prepare() {
// transforms the array of gestures into array of grouped raw (not composed) gestures
// i.e. [gesture1, gesture2, ComposedGesture(gesture3, gesture4)] -> [[gesture1], [gesture2], [gesture3, gesture4]]
const gestureArrays = this.gestures.map((gesture) =>
gesture.toGestureArray()
);
let requireToFail: GestureType[] = [];
for (let i = 0; i < this.gestures.length; i++) {
this.prepareSingleGesture(
this.gestures[i],
this.simultaneousGestures,
this.requireGesturesToFail.concat(requireToFail)
);
// every group gets to wait for all groups before it
requireToFail = requireToFail.concat(gestureArrays[i]);
}
}
}
export type ComposedGestureType = InstanceType<typeof ComposedGesture>;
export type RaceGestureType = ComposedGestureType;
export type SimultaneousGestureType = InstanceType<typeof SimultaneousGesture>;
export type ExclusiveGestureType = InstanceType<typeof ExclusiveGesture>;

View File

@@ -0,0 +1,141 @@
import { FlingGesture } from './flingGesture';
import { ForceTouchGesture } from './forceTouchGesture';
import { Gesture } from './gesture';
import {
ComposedGesture,
ExclusiveGesture,
SimultaneousGesture,
} from './gestureComposition';
import { LongPressGesture } from './longPressGesture';
import { PanGesture } from './panGesture';
import { PinchGesture } from './pinchGesture';
import { RotationGesture } from './rotationGesture';
import { TapGesture } from './tapGesture';
import { NativeGesture } from './nativeGesture';
import { ManualGesture } from './manualGesture';
import { HoverGesture } from './hoverGesture';
/**
* `Gesture` is the object that allows you to create and compose gestures.
*
* ### Remarks
* - Consider wrapping your gesture configurations with `useMemo`, as it will reduce the amount of work Gesture Handler has to do under the hood when updating gestures.
*
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/gesture
*/
export const GestureObjects = {
/**
* A discrete gesture that recognizes one or many taps.
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/tap-gesture
*/
Tap: () => {
return new TapGesture();
},
/**
* A continuous gesture that can recognize a panning (dragging) gesture and track its movement.
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/pan-gesture
*/
Pan: () => {
return new PanGesture();
},
/**
* A continuous gesture that recognizes pinch gesture. It allows for tracking the distance between two fingers and use that information to scale or zoom your content.
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/pinch-gesture
*/
Pinch: () => {
return new PinchGesture();
},
/**
* A continuous gesture that can recognize rotation and track its movement.
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/rotation-gesture
*/
Rotation: () => {
return new RotationGesture();
},
/**
* A discrete gesture that activates when the movement is sufficiently fast.
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/fling-gesture
*/
Fling: () => {
return new FlingGesture();
},
/**
* A discrete gesture that activates when the corresponding view is pressed for a sufficiently long time.
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/long-press-gesture
*/
LongPress: () => {
return new LongPressGesture();
},
/**
* #### iOS only
* A continuous gesture that recognizes force of a touch. It allows for tracking pressure of touch on some iOS devices.
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/force-touch-gesture
*/
ForceTouch: () => {
return new ForceTouchGesture();
},
/**
* A gesture that allows other touch handling components to participate in RNGH's gesture system.
* When used, the other component should be the direct child of a `GestureDetector`.
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/native-gesture
*/
Native: () => {
return new NativeGesture();
},
/**
* A plain gesture that has no specific activation criteria nor event data set.
* Its state has to be controlled manually using a state manager.
* It will not fail when all the pointers are lifted from the screen.
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/manual-gesture
*/
Manual: () => {
return new ManualGesture();
},
/**
* A continuous gesture that can recognize hovering above the view it's attached to.
* The hover effect may be activated by moving a mouse or a stylus over the view.
*
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/hover-gesture
*/
Hover: () => {
return new HoverGesture();
},
/**
* Builds a composed gesture consisting of gestures provided as parameters.
* The first one that becomes active cancels the rest of gestures.
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/gesture-composition/#race
*/
Race: (...gestures: Gesture[]) => {
return new ComposedGesture(...gestures);
},
/**
* Builds a composed gesture that allows all base gestures to run simultaneously.
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/gesture-composition/#simultaneous
*/
Simultaneous(...gestures: Gesture[]) {
return new SimultaneousGesture(...gestures);
},
/**
* Builds a composed gesture where only one of the provided gestures can become active.
* Priority is decided through the order of gestures: the first one has higher priority
* than the second one, second one has higher priority than the third one, and so on.
* For example, to make a gesture that recognizes both single and double tap you need
* to call Exclusive(doubleTap, singleTap).
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/gesture-composition/#exclusive
*/
Exclusive(...gestures: Gesture[]) {
return new ExclusiveGesture(...gestures);
},
};

View File

@@ -0,0 +1,64 @@
import { Reanimated } from './reanimatedWrapper';
import { State } from '../../State';
import { tagMessage } from '../../utils';
export interface GestureStateManagerType {
begin: () => void;
activate: () => void;
fail: () => void;
end: () => void;
}
const warningMessage = tagMessage(
'react-native-reanimated is required in order to use synchronous state management'
);
// check if reanimated module is available, but look for useSharedValue as conditional
// require of reanimated can sometimes return content of `utils.ts` file (?)
const REANIMATED_AVAILABLE = Reanimated?.useSharedValue !== undefined;
const setGestureState = Reanimated?.setGestureState;
function create(handlerTag: number): GestureStateManagerType {
'worklet';
return {
begin: () => {
'worklet';
if (REANIMATED_AVAILABLE) {
setGestureState(handlerTag, State.BEGAN);
} else {
console.warn(warningMessage);
}
},
activate: () => {
'worklet';
if (REANIMATED_AVAILABLE) {
setGestureState(handlerTag, State.ACTIVE);
} else {
console.warn(warningMessage);
}
},
fail: () => {
'worklet';
if (REANIMATED_AVAILABLE) {
setGestureState(handlerTag, State.FAILED);
} else {
console.warn(warningMessage);
}
},
end: () => {
'worklet';
if (REANIMATED_AVAILABLE) {
setGestureState(handlerTag, State.END);
} else {
console.warn(warningMessage);
}
},
};
}
export const GestureStateManager = {
create,
};

View File

@@ -0,0 +1,24 @@
import NodeManager from '../../web/tools/NodeManager';
import { GestureStateManagerType } from './gestureStateManager';
export const GestureStateManager = {
create(handlerTag: number): GestureStateManagerType {
return {
begin: () => {
NodeManager.getHandler(handlerTag).begin();
},
activate: () => {
NodeManager.getHandler(handlerTag).activate(true);
},
fail: () => {
NodeManager.getHandler(handlerTag).fail();
},
end: () => {
NodeManager.getHandler(handlerTag).end();
},
};
},
};

View File

@@ -0,0 +1,83 @@
import { BaseGestureConfig, ContinousBaseGesture } from './gesture';
import { GestureUpdateEvent } from '../gestureHandlerCommon';
export type HoverGestureHandlerEventPayload = {
x: number;
y: number;
absoluteX: number;
absoluteY: number;
};
export type HoverGestureChangeEventPayload = {
changeX: number;
changeY: number;
};
export enum HoverEffect {
NONE = 0,
LIFT = 1,
HIGHLIGHT = 2,
}
export interface HoverGestureConfig {
hoverEffect?: HoverEffect;
}
export const hoverGestureHandlerProps = ['hoverEffect'] as const;
function changeEventCalculator(
current: GestureUpdateEvent<HoverGestureHandlerEventPayload>,
previous?: GestureUpdateEvent<HoverGestureHandlerEventPayload>
) {
'worklet';
let changePayload: HoverGestureChangeEventPayload;
if (previous === undefined) {
changePayload = {
changeX: current.x,
changeY: current.y,
};
} else {
changePayload = {
changeX: current.x - previous.x,
changeY: current.y - previous.y,
};
}
return { ...current, ...changePayload };
}
export class HoverGesture extends ContinousBaseGesture<
HoverGestureHandlerEventPayload,
HoverGestureChangeEventPayload
> {
public config: BaseGestureConfig & HoverGestureConfig = {};
constructor() {
super();
this.handlerName = 'HoverGestureHandler';
}
/**
* #### iOS only
* Sets the visual hover effect.
*/
effect(effect: HoverEffect) {
this.config.hoverEffect = effect;
return this;
}
onChange(
callback: (
event: GestureUpdateEvent<
HoverGestureHandlerEventPayload & HoverGestureChangeEventPayload
>
) => void
) {
// @ts-ignore TS being overprotective, HoverGestureHandlerEventPayload is Record
this.handlers.changeEventCalculator = changeEventCalculator;
return super.onChange(callback);
}
}
export type HoverGestureType = InstanceType<typeof HoverGesture>;

View File

@@ -0,0 +1,38 @@
import { BaseGesture, BaseGestureConfig } from './gesture';
import {
LongPressGestureConfig,
LongPressGestureHandlerEventPayload,
} from '../LongPressGestureHandler';
export class LongPressGesture extends BaseGesture<LongPressGestureHandlerEventPayload> {
public config: BaseGestureConfig & LongPressGestureConfig = {};
constructor() {
super();
this.handlerName = 'LongPressGestureHandler';
this.shouldCancelWhenOutside(true);
}
/**
* Minimum time, expressed in milliseconds, that a finger must remain pressed on the corresponding view.
* The default value is 500.
* @param duration
*/
minDuration(duration: number) {
this.config.minDurationMs = duration;
return this;
}
/**
* Maximum distance, expressed in points, that defines how far the finger is allowed to travel during a long press gesture.
* @param distance
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/long-press-gesture#maxdistancevalue-number
*/
maxDistance(distance: number) {
this.config.maxDist = distance;
return this;
}
}
export type LongPressGestureType = InstanceType<typeof LongPressGesture>;

View File

@@ -0,0 +1,31 @@
import { GestureUpdateEvent } from '../gestureHandlerCommon';
import { ContinousBaseGesture } from './gesture';
function changeEventCalculator(
current: GestureUpdateEvent<Record<string, never>>,
_previous?: GestureUpdateEvent<Record<string, never>>
) {
'worklet';
return current;
}
export class ManualGesture extends ContinousBaseGesture<
Record<string, never>,
Record<string, never>
> {
constructor() {
super();
this.handlerName = 'ManualGestureHandler';
}
onChange(
callback: (event: GestureUpdateEvent<Record<string, never>>) => void
) {
// @ts-ignore TS being overprotective, Record<string, never> is Record
this.handlers.changeEventCalculator = changeEventCalculator;
return super.onChange(callback);
}
}
export type ManualGestureType = InstanceType<typeof ManualGesture>;

View File

@@ -0,0 +1,35 @@
import { BaseGestureConfig, BaseGesture } from './gesture';
import {
NativeViewGestureConfig,
NativeViewGestureHandlerPayload,
} from '../NativeViewGestureHandler';
export class NativeGesture extends BaseGesture<NativeViewGestureHandlerPayload> {
public config: BaseGestureConfig & NativeViewGestureConfig = {};
constructor() {
super();
this.handlerName = 'NativeViewGestureHandler';
}
/**
* When true, underlying handler will activate unconditionally when in `BEGAN` or `UNDETERMINED` state.
* @param value
*/
shouldActivateOnStart(value: boolean) {
this.config.shouldActivateOnStart = value;
return this;
}
/**
* When true, cancels all other gesture handlers when this `NativeViewGestureHandler` receives an `ACTIVE` state event.
* @param value
*/
disallowInterruption(value: boolean) {
this.config.disallowInterruption = value;
return this;
}
}
export type NativeGestureType = InstanceType<typeof NativeGesture>;

View File

@@ -0,0 +1,223 @@
import { BaseGestureConfig, ContinousBaseGesture } from './gesture';
import { GestureUpdateEvent } from '../gestureHandlerCommon';
import {
PanGestureConfig,
PanGestureHandlerEventPayload,
} from '../PanGestureHandler';
export type PanGestureChangeEventPayload = {
changeX: number;
changeY: number;
};
function changeEventCalculator(
current: GestureUpdateEvent<PanGestureHandlerEventPayload>,
previous?: GestureUpdateEvent<PanGestureHandlerEventPayload>
) {
'worklet';
let changePayload: PanGestureChangeEventPayload;
if (previous === undefined) {
changePayload = {
changeX: current.translationX,
changeY: current.translationY,
};
} else {
changePayload = {
changeX: current.translationX - previous.translationX,
changeY: current.translationY - previous.translationY,
};
}
return { ...current, ...changePayload };
}
export class PanGesture extends ContinousBaseGesture<
PanGestureHandlerEventPayload,
PanGestureChangeEventPayload
> {
public config: BaseGestureConfig & PanGestureConfig = {};
constructor() {
super();
this.handlerName = 'PanGestureHandler';
}
/**
* Range along Y axis (in points) where fingers travels without activation of gesture.
* @param offset
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/pan-gesture#activeoffsetyvalue-number--number
*/
activeOffsetY(
offset: number | [activeOffsetYStart: number, activeOffsetYEnd: number]
) {
if (Array.isArray(offset)) {
this.config.activeOffsetYStart = offset[0];
this.config.activeOffsetYEnd = offset[1];
} else if (offset < 0) {
this.config.activeOffsetYStart = offset;
} else {
this.config.activeOffsetYEnd = offset;
}
return this;
}
/**
* Range along X axis (in points) where fingers travels without activation of gesture.
* @param offset
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/pan-gesture#activeoffsetxvalue-number--number
*/
activeOffsetX(
offset: number | [activeOffsetXStart: number, activeOffsetXEnd: number]
) {
if (Array.isArray(offset)) {
this.config.activeOffsetXStart = offset[0];
this.config.activeOffsetXEnd = offset[1];
} else if (offset < 0) {
this.config.activeOffsetXStart = offset;
} else {
this.config.activeOffsetXEnd = offset;
}
return this;
}
/**
* When the finger moves outside this range (in points) along Y axis and gesture hasn't yet activated it will fail recognizing the gesture.
* @param offset
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/pan-gesture#failoffsetyvalue-number--number
*/
failOffsetY(
offset: number | [failOffsetYStart: number, failOffsetYEnd: number]
) {
if (Array.isArray(offset)) {
this.config.failOffsetYStart = offset[0];
this.config.failOffsetYEnd = offset[1];
} else if (offset < 0) {
this.config.failOffsetYStart = offset;
} else {
this.config.failOffsetYEnd = offset;
}
return this;
}
/**
* When the finger moves outside this range (in points) along X axis and gesture hasn't yet activated it will fail recognizing the gesture.
* @param offset
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/pan-gesture#failoffsetxvalue-number--number
*/
failOffsetX(
offset: number | [failOffsetXStart: number, failOffsetXEnd: number]
) {
if (Array.isArray(offset)) {
this.config.failOffsetXStart = offset[0];
this.config.failOffsetXEnd = offset[1];
} else if (offset < 0) {
this.config.failOffsetXStart = offset;
} else {
this.config.failOffsetXEnd = offset;
}
return this;
}
/**
* A number of fingers that is required to be placed before gesture can activate. Should be a higher or equal to 0 integer.
* @param minPointers
*/
minPointers(minPointers: number) {
this.config.minPointers = minPointers;
return this;
}
/**
* When the given number of fingers is placed on the screen and gesture hasn't yet activated it will fail recognizing the gesture.
* Should be a higher or equal to 0 integer.
* @param maxPointers
*/
maxPointers(maxPointers: number) {
this.config.maxPointers = maxPointers;
return this;
}
/**
* Minimum distance the finger (or multiple finger) need to travel before the gesture activates.
* Expressed in points.
* @param distance
*/
minDistance(distance: number) {
this.config.minDist = distance;
return this;
}
/**
* Minimum velocity the finger has to reach in order to activate handler.
* @param velocity
*/
minVelocity(velocity: number) {
this.config.minVelocity = velocity;
return this;
}
/**
* Minimum velocity along X axis the finger has to reach in order to activate handler.
* @param velocity
*/
minVelocityX(velocity: number) {
this.config.minVelocityX = velocity;
return this;
}
/**
* Minimum velocity along Y axis the finger has to reach in order to activate handler.
* @param velocity
*/
minVelocityY(velocity: number) {
this.config.minVelocityY = velocity;
return this;
}
/**
* #### Android only
* Android, by default, will calculate translation values based on the position of the leading pointer (the first one that was placed on the screen).
* This modifier allows that behavior to be changed to the one that is default on iOS - the averaged position of all active pointers will be used to calculate the translation values.
* @param value
*/
averageTouches(value: boolean) {
this.config.avgTouches = value;
return this;
}
/**
* #### iOS only
* Enables two-finger gestures on supported devices, for example iPads with trackpads.
* @param value
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/pan-gesture/#enabletrackpadtwofingergesturevalue-boolean-ios-only
*/
enableTrackpadTwoFingerGesture(value: boolean) {
this.config.enableTrackpadTwoFingerGesture = value;
return this;
}
/**
* Duration in milliseconds of the LongPress gesture before Pan is allowed to activate.
* @param duration
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/pan-gesture/#activateafterlongpressduration-number
*/
activateAfterLongPress(duration: number) {
this.config.activateAfterLongPress = duration;
return this;
}
onChange(
callback: (
event: GestureUpdateEvent<
PanGestureHandlerEventPayload & PanGestureChangeEventPayload
>
) => void
) {
// @ts-ignore TS being overprotective, PanGestureHandlerEventPayload is Record
this.handlers.changeEventCalculator = changeEventCalculator;
return super.onChange(callback);
}
}
export type PanGestureType = InstanceType<typeof PanGesture>;

View File

@@ -0,0 +1,51 @@
import { ContinousBaseGesture } from './gesture';
import { PinchGestureHandlerEventPayload } from '../PinchGestureHandler';
import { GestureUpdateEvent } from '../gestureHandlerCommon';
export type PinchGestureChangeEventPayload = {
scaleChange: number;
};
function changeEventCalculator(
current: GestureUpdateEvent<PinchGestureHandlerEventPayload>,
previous?: GestureUpdateEvent<PinchGestureHandlerEventPayload>
) {
'worklet';
let changePayload: PinchGestureChangeEventPayload;
if (previous === undefined) {
changePayload = {
scaleChange: current.scale,
};
} else {
changePayload = {
scaleChange: current.scale / previous.scale,
};
}
return { ...current, ...changePayload };
}
export class PinchGesture extends ContinousBaseGesture<
PinchGestureHandlerEventPayload,
PinchGestureChangeEventPayload
> {
constructor() {
super();
this.handlerName = 'PinchGestureHandler';
}
onChange(
callback: (
event: GestureUpdateEvent<
PinchGestureHandlerEventPayload & PinchGestureChangeEventPayload
>
) => void
) {
// @ts-ignore TS being overprotective, PinchGestureHandlerEventPayload is Record
this.handlers.changeEventCalculator = changeEventCalculator;
return super.onChange(callback);
}
}
export type PinchGestureType = InstanceType<typeof PinchGesture>;

View File

@@ -0,0 +1,56 @@
import { ComponentClass } from 'react';
import {
GestureUpdateEvent,
GestureStateChangeEvent,
} from '../gestureHandlerCommon';
import { tagMessage } from '../../utils';
export interface SharedValue<T> {
value: T;
}
let Reanimated: {
default: {
// Slightly modified definition copied from 'react-native-reanimated'
// eslint-disable-next-line @typescript-eslint/ban-types
createAnimatedComponent<P extends object>(
component: ComponentClass<P>,
options?: unknown
): ComponentClass<P>;
};
useEvent: (
callback: (event: GestureUpdateEvent | GestureStateChangeEvent) => void,
events: string[],
rebuild: boolean
) => unknown;
useSharedValue: <T>(value: T) => SharedValue<T>;
setGestureState: (handlerTag: number, newState: number) => void;
};
try {
Reanimated = require('react-native-reanimated');
} catch (e) {
// When 'react-native-reanimated' is not available we want to quietly continue
// @ts-ignore TS demands the variable to be initialized
Reanimated = undefined;
}
if (!Reanimated?.useSharedValue) {
// @ts-ignore Make sure the loaded module is actually Reanimated, if it's not
// reset the module to undefined so we can fallback to the default implementation
Reanimated = undefined;
}
if (Reanimated !== undefined && !Reanimated.setGestureState) {
// The loaded module is Reanimated but it doesn't have the setGestureState defined
Reanimated.setGestureState = () => {
'worklet';
console.warn(
tagMessage(
'Please use newer version of react-native-reanimated in order to control state of the gestures.'
)
);
};
}
export { Reanimated };

View File

@@ -0,0 +1,51 @@
import { ContinousBaseGesture } from './gesture';
import { RotationGestureHandlerEventPayload } from '../RotationGestureHandler';
import { GestureUpdateEvent } from '../gestureHandlerCommon';
type RotationGestureChangeEventPayload = {
rotationChange: number;
};
function changeEventCalculator(
current: GestureUpdateEvent<RotationGestureHandlerEventPayload>,
previous?: GestureUpdateEvent<RotationGestureHandlerEventPayload>
) {
'worklet';
let changePayload: RotationGestureChangeEventPayload;
if (previous === undefined) {
changePayload = {
rotationChange: current.rotation,
};
} else {
changePayload = {
rotationChange: current.rotation - previous.rotation,
};
}
return { ...current, ...changePayload };
}
export class RotationGesture extends ContinousBaseGesture<
RotationGestureHandlerEventPayload,
RotationGestureChangeEventPayload
> {
constructor() {
super();
this.handlerName = 'RotationGestureHandler';
}
onChange(
callback: (
event: GestureUpdateEvent<
RotationGestureHandlerEventPayload & RotationGestureChangeEventPayload
>
) => void
) {
// @ts-ignore TS being overprotective, RotationGestureHandlerEventPayload is Record
this.handlers.changeEventCalculator = changeEventCalculator;
return super.onChange(callback);
}
}
export type RotationGestureType = InstanceType<typeof RotationGesture>;

View File

@@ -0,0 +1,88 @@
import { BaseGestureConfig, BaseGesture } from './gesture';
import {
TapGestureConfig,
TapGestureHandlerEventPayload,
} from '../TapGestureHandler';
export class TapGesture extends BaseGesture<TapGestureHandlerEventPayload> {
public config: BaseGestureConfig & TapGestureConfig = {};
constructor() {
super();
this.handlerName = 'TapGestureHandler';
this.shouldCancelWhenOutside(true);
}
/**
* Minimum number of pointers (fingers) required to be placed before the gesture activates.
* Should be a positive integer. The default value is 1.
* @param minPointers
*/
minPointers(minPointers: number) {
this.config.minPointers = minPointers;
return this;
}
/**
* Number of tap gestures required to activate the gesture.
* The default value is 1.
* @param count
*/
numberOfTaps(count: number) {
this.config.numberOfTaps = count;
return this;
}
/**
* Maximum distance, expressed in points, that defines how far the finger is allowed to travel during a tap gesture.
* @param maxDist
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/tap-gesture#maxdistancevalue-number
*/
maxDistance(maxDist: number) {
this.config.maxDist = maxDist;
return this;
}
/**
* Maximum time, expressed in milliseconds, that defines how fast a finger must be released after a touch.
* The default value is 500.
* @param duration
*/
maxDuration(duration: number) {
this.config.maxDurationMs = duration;
return this;
}
/**
* Maximum time, expressed in milliseconds, that can pass before the next tap — if many taps are required.
* The default value is 500.
* @param delay
*/
maxDelay(delay: number) {
this.config.maxDelayMs = delay;
return this;
}
/**
* Maximum distance, expressed in points, that defines how far the finger is allowed to travel along the X axis during a tap gesture.
* @param delta
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/tap-gesture#maxdeltaxvalue-number
*/
maxDeltaX(delta: number) {
this.config.maxDeltaX = delta;
return this;
}
/**
* Maximum distance, expressed in points, that defines how far the finger is allowed to travel along the Y axis during a tap gesture.
* @param delta
* @see https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/tap-gesture#maxdeltayvalue-number
*/
maxDeltaY(delta: number) {
this.config.maxDeltaY = delta;
return this;
}
}
export type TapGestureType = InstanceType<typeof TapGesture>;

View File

@@ -0,0 +1,60 @@
import { isJestEnv } from '../utils';
import { GestureType } from './gestures/gesture';
import { GestureEvent, HandlerStateChangeEvent } from './gestureHandlerCommon';
export const handlerIDToTag: Record<string, number> = {};
const gestures = new Map<number, GestureType>();
const oldHandlers = new Map<number, GestureHandlerCallbacks>();
const testIDs = new Map<string, number>();
let handlerTag = 1;
export function getNextHandlerTag(): number {
return handlerTag++;
}
export function registerHandler(
handlerTag: number,
handler: GestureType,
testID?: string
) {
gestures.set(handlerTag, handler);
if (isJestEnv() && testID) {
testIDs.set(testID, handlerTag);
}
}
export function registerOldGestureHandler(
handlerTag: number,
handler: GestureHandlerCallbacks
) {
oldHandlers.set(handlerTag, handler);
}
export function unregisterHandler(handlerTag: number, testID?: string) {
gestures.delete(handlerTag);
if (isJestEnv() && testID) {
testIDs.delete(testID);
}
}
export function findHandler(handlerTag: number) {
return gestures.get(handlerTag);
}
export function findOldGestureHandler(handlerTag: number) {
return oldHandlers.get(handlerTag);
}
export function findHandlerByTestID(testID: string) {
const handlerTag = testIDs.get(testID);
if (handlerTag !== undefined) {
return findHandler(handlerTag) ?? null;
}
return null;
}
export interface GestureHandlerCallbacks {
onGestureEvent: (event: GestureEvent<any>) => void;
onGestureStateChange: (event: HandlerStateChangeEvent<any>) => void;
}

View File

@@ -0,0 +1,176 @@
import { initialize } from './init';
export { Directions } from './Directions';
export { State } from './State';
export { PointerType } from './PointerType';
export { default as gestureHandlerRootHOC } from './components/gestureHandlerRootHOC';
export { default as GestureHandlerRootView } from './components/GestureHandlerRootView';
export type {
// event types
GestureEvent,
HandlerStateChangeEvent,
// event payloads types
GestureEventPayload,
HandlerStateChangeEventPayload,
// pointer events
GestureTouchEvent,
TouchData,
// new api event types
GestureUpdateEvent,
GestureStateChangeEvent,
} from './handlers/gestureHandlerCommon';
export { MouseButton } from './handlers/gestureHandlerCommon';
export type { GestureType } from './handlers/gestures/gesture';
export type {
TapGestureHandlerEventPayload,
TapGestureHandlerProps,
} from './handlers/TapGestureHandler';
export type {
ForceTouchGestureHandlerEventPayload,
ForceTouchGestureHandlerProps,
} from './handlers/ForceTouchGestureHandler';
export type { ForceTouchGestureChangeEventPayload } from './handlers/gestures/forceTouchGesture';
export type {
LongPressGestureHandlerEventPayload,
LongPressGestureHandlerProps,
} from './handlers/LongPressGestureHandler';
export type {
PanGestureHandlerEventPayload,
PanGestureHandlerProps,
} from './handlers/PanGestureHandler';
export type { PanGestureChangeEventPayload } from './handlers/gestures/panGesture';
export type {
PinchGestureHandlerEventPayload,
PinchGestureHandlerProps,
} from './handlers/PinchGestureHandler';
export type { PinchGestureChangeEventPayload } from './handlers/gestures/pinchGesture';
export type {
RotationGestureHandlerEventPayload,
RotationGestureHandlerProps,
} from './handlers/RotationGestureHandler';
export type {
FlingGestureHandlerEventPayload,
FlingGestureHandlerProps,
} from './handlers/FlingGestureHandler';
export { TapGestureHandler } from './handlers/TapGestureHandler';
export { ForceTouchGestureHandler } from './handlers/ForceTouchGestureHandler';
export { LongPressGestureHandler } from './handlers/LongPressGestureHandler';
export { PanGestureHandler } from './handlers/PanGestureHandler';
export { PinchGestureHandler } from './handlers/PinchGestureHandler';
export { RotationGestureHandler } from './handlers/RotationGestureHandler';
export { FlingGestureHandler } from './handlers/FlingGestureHandler';
export { default as createNativeWrapper } from './handlers/createNativeWrapper';
export type {
NativeViewGestureHandlerPayload,
NativeViewGestureHandlerProps,
} from './handlers/NativeViewGestureHandler';
export { GestureDetector } from './handlers/gestures/GestureDetector';
export { GestureObjects as Gesture } from './handlers/gestures/gestureObjects';
export type { TapGestureType as TapGesture } from './handlers/gestures/tapGesture';
export type { PanGestureType as PanGesture } from './handlers/gestures/panGesture';
export type { FlingGestureType as FlingGesture } from './handlers/gestures/flingGesture';
export type { LongPressGestureType as LongPressGesture } from './handlers/gestures/longPressGesture';
export type { PinchGestureType as PinchGesture } from './handlers/gestures/pinchGesture';
export type { RotationGestureType as RotationGesture } from './handlers/gestures/rotationGesture';
export type { ForceTouchGestureType as ForceTouchGesture } from './handlers/gestures/forceTouchGesture';
export type { NativeGestureType as NativeGesture } from './handlers/gestures/nativeGesture';
export type { ManualGestureType as ManualGesture } from './handlers/gestures/manualGesture';
export type { HoverGestureType as HoverGesture } from './handlers/gestures/hoverGesture';
export type {
ComposedGestureType as ComposedGesture,
RaceGestureType as RaceGesture,
SimultaneousGestureType as SimultaneousGesture,
ExclusiveGestureType as ExclusiveGesture,
} from './handlers/gestures/gestureComposition';
export type { GestureStateManagerType as GestureStateManager } from './handlers/gestures/gestureStateManager';
export { NativeViewGestureHandler } from './handlers/NativeViewGestureHandler';
export type {
RawButtonProps,
BaseButtonProps,
RectButtonProps,
BorderlessButtonProps,
} from './components/GestureButtons';
export {
RawButton,
BaseButton,
RectButton,
BorderlessButton,
PureNativeButton,
} from './components/GestureButtons';
export type {
TouchableHighlightProps,
TouchableOpacityProps,
TouchableWithoutFeedbackProps,
} from './components/touchables';
export {
TouchableHighlight,
TouchableNativeFeedback,
TouchableOpacity,
TouchableWithoutFeedback,
} from './components/touchables';
export {
ScrollView,
Switch,
TextInput,
DrawerLayoutAndroid,
FlatList,
RefreshControl,
} from './components/GestureComponents';
export { HoverEffect } from './handlers/gestures/hoverGesture';
export type {
//events
GestureHandlerGestureEvent,
GestureHandlerStateChangeEvent,
//event payloads
GestureHandlerGestureEventNativeEvent,
GestureHandlerStateChangeNativeEvent,
NativeViewGestureHandlerGestureEvent,
NativeViewGestureHandlerStateChangeEvent,
TapGestureHandlerGestureEvent,
TapGestureHandlerStateChangeEvent,
ForceTouchGestureHandlerGestureEvent,
ForceTouchGestureHandlerStateChangeEvent,
LongPressGestureHandlerGestureEvent,
LongPressGestureHandlerStateChangeEvent,
PanGestureHandlerGestureEvent,
PanGestureHandlerStateChangeEvent,
PinchGestureHandlerGestureEvent,
PinchGestureHandlerStateChangeEvent,
RotationGestureHandlerGestureEvent,
RotationGestureHandlerStateChangeEvent,
FlingGestureHandlerGestureEvent,
FlingGestureHandlerStateChangeEvent,
// handlers props
NativeViewGestureHandlerProperties,
TapGestureHandlerProperties,
LongPressGestureHandlerProperties,
PanGestureHandlerProperties,
PinchGestureHandlerProperties,
RotationGestureHandlerProperties,
FlingGestureHandlerProperties,
ForceTouchGestureHandlerProperties,
// buttons props
RawButtonProperties,
BaseButtonProperties,
RectButtonProperties,
BorderlessButtonProperties,
} from './handlers/gestureHandlerTypesCompat';
export type { SwipeableProps } from './components/Swipeable';
export { default as Swipeable } from './components/Swipeable';
export type {
DrawerLayoutProps,
DrawerPosition,
DrawerState,
DrawerType,
DrawerLockMode,
DrawerKeyboardDismissMode,
} from './components/DrawerLayout';
export { default as DrawerLayout } from './components/DrawerLayout';
export {
enableExperimentalWebImplementation,
enableLegacyWebImplementation,
} from './EnableNewWebImplementation';
initialize();

View File

@@ -0,0 +1,18 @@
import { startListening } from './handlers/gestures/eventReceiver';
import RNGestureHandlerModule from './RNGestureHandlerModule';
import { isFabric } from './utils';
let fabricInitialized = false;
export function initialize() {
startListening();
}
// since isFabric() may give wrong results before the first render, we call this
// method during render of GestureHandlerRootView
export function maybeInitializeFabric() {
if (isFabric() && !fabricInitialized) {
RNGestureHandlerModule.install();
fabricInitialized = true;
}
}

View File

@@ -0,0 +1 @@
export { getByGestureTestId, fireGestureHandler } from './jestUtils';

View File

@@ -0,0 +1,505 @@
import invariant from 'invariant';
import { DeviceEventEmitter } from 'react-native';
import { ReactTestInstance } from 'react-test-renderer';
import {
FlingGestureHandler,
FlingGestureHandlerEventPayload,
flingHandlerName,
} from '../handlers/FlingGestureHandler';
import {
ForceTouchGestureHandler,
ForceTouchGestureHandlerEventPayload,
forceTouchHandlerName,
} from '../handlers/ForceTouchGestureHandler';
import {
BaseGestureHandlerProps,
GestureEvent,
HandlerStateChangeEvent,
} from '../handlers/gestureHandlerCommon';
import { FlingGesture } from '../handlers/gestures/flingGesture';
import { ForceTouchGesture } from '../handlers/gestures/forceTouchGesture';
import { BaseGesture, GestureType } from '../handlers/gestures/gesture';
import { LongPressGesture } from '../handlers/gestures/longPressGesture';
import { NativeGesture } from '../handlers/gestures/nativeGesture';
import { PanGesture } from '../handlers/gestures/panGesture';
import { PinchGesture } from '../handlers/gestures/pinchGesture';
import { RotationGesture } from '../handlers/gestures/rotationGesture';
import { TapGesture } from '../handlers/gestures/tapGesture';
import { findHandlerByTestID } from '../handlers/handlersRegistry';
import {
LongPressGestureHandler,
LongPressGestureHandlerEventPayload,
longPressHandlerName,
} from '../handlers/LongPressGestureHandler';
import {
NativeViewGestureHandler,
NativeViewGestureHandlerPayload,
nativeViewHandlerName,
} from '../handlers/NativeViewGestureHandler';
import {
PanGestureHandler,
PanGestureHandlerEventPayload,
panHandlerName,
} from '../handlers/PanGestureHandler';
import {
PinchGestureHandler,
PinchGestureHandlerEventPayload,
pinchHandlerName,
} from '../handlers/PinchGestureHandler';
import {
RotationGestureHandler,
RotationGestureHandlerEventPayload,
rotationHandlerName,
} from '../handlers/RotationGestureHandler';
import {
TapGestureHandler,
TapGestureHandlerEventPayload,
tapHandlerName,
} from '../handlers/TapGestureHandler';
import { State } from '../State';
import { hasProperty, withPrevAndCurrent } from '../utils';
// load fireEvent conditionally, so RNGH may be used in setups without testing-library
let fireEvent = (
_element: ReactTestInstance,
_name: string,
..._data: any[]
) => {
// NOOP
};
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
fireEvent = require('@testing-library/react-native').fireEvent;
} catch (_e) {
// do nothing if not available
}
type GestureHandlerTestEvent<
TEventPayload extends Record<string, unknown> = Record<string, unknown>
> = (
| GestureEvent<TEventPayload>
| HandlerStateChangeEvent<TEventPayload>
)['nativeEvent'];
type HandlerNames = keyof DefaultEventsMapping;
type WithNumberOfPointers<T> = {
[P in keyof T]: T[P] & { numberOfPointers: number };
};
type DefaultEventsMapping = WithNumberOfPointers<{
[flingHandlerName]: FlingGestureHandlerEventPayload;
[forceTouchHandlerName]: ForceTouchGestureHandlerEventPayload;
[longPressHandlerName]: LongPressGestureHandlerEventPayload;
[nativeViewHandlerName]: NativeViewGestureHandlerPayload;
[panHandlerName]: PanGestureHandlerEventPayload;
[pinchHandlerName]: PinchGestureHandlerEventPayload;
[rotationHandlerName]: RotationGestureHandlerEventPayload;
[tapHandlerName]: TapGestureHandlerEventPayload;
}>;
const handlersDefaultEvents: DefaultEventsMapping = {
[flingHandlerName]: {
x: 0,
y: 0,
absoluteX: 0,
absoluteY: 0,
numberOfPointers: 1,
},
[forceTouchHandlerName]: {
x: 0,
y: 0,
absoluteX: 0,
absoluteY: 0,
force: 1,
numberOfPointers: 1,
},
[longPressHandlerName]: {
x: 0,
y: 0,
absoluteX: 0,
absoluteY: 0,
duration: 100,
numberOfPointers: 1,
},
[nativeViewHandlerName]: {
pointerInside: true,
numberOfPointers: 1,
},
[panHandlerName]: {
x: 0,
y: 0,
absoluteX: 0,
absoluteY: 0,
translationX: 100,
translationY: 0,
velocityX: 3,
velocityY: 0,
numberOfPointers: 1,
},
[pinchHandlerName]: {
focalX: 0,
focalY: 0,
scale: 2,
velocity: 1,
numberOfPointers: 2,
},
[rotationHandlerName]: {
anchorX: 0,
anchorY: 0,
rotation: 3.14,
velocity: 2,
numberOfPointers: 2,
},
[tapHandlerName]: {
x: 0,
y: 0,
absoluteX: 0,
absoluteY: 0,
numberOfPointers: 1,
},
};
function isGesture(
componentOrGesture: ReactTestInstance | GestureType
): componentOrGesture is GestureType {
return componentOrGesture instanceof BaseGesture;
}
interface WrappedGestureHandlerTestEvent {
nativeEvent: GestureHandlerTestEvent;
}
function wrapWithNativeEvent(
event: GestureHandlerTestEvent
): WrappedGestureHandlerTestEvent {
return { nativeEvent: event };
}
function fillOldStateChanges(
previousEvent: GestureHandlerTestEvent | null,
currentEvent: Omit<GestureHandlerTestEvent, 'oldState'>
): GestureHandlerTestEvent {
const isFirstEvent = previousEvent === null;
if (isFirstEvent) {
return {
oldState: State.UNDETERMINED,
...currentEvent,
} as GestureHandlerTestEvent;
}
const isGestureStateEvent = previousEvent.state !== currentEvent.state;
if (isGestureStateEvent) {
return {
oldState: previousEvent?.state,
...currentEvent,
} as GestureHandlerTestEvent;
} else {
return currentEvent as GestureHandlerTestEvent;
}
}
type EventWithStates = Partial<
Pick<GestureHandlerTestEvent, 'oldState' | 'state'>
>;
function validateStateTransitions(
previousEvent: EventWithStates | null,
currentEvent: EventWithStates
) {
function stringify(event: Record<string, unknown> | null) {
return JSON.stringify(event, null, 2);
}
function errorMsgWithBothEvents(description: string) {
return `${description}, invalid event: ${stringify(
currentEvent
)}, previous event: ${stringify(previousEvent)}`;
}
function errorMsgWithCurrentEvent(description: string) {
return `${description}, invalid event: ${stringify(currentEvent)}`;
}
invariant(
hasProperty(currentEvent, 'state'),
errorMsgWithCurrentEvent('every event must have state')
);
const isFirstEvent = previousEvent === null;
if (isFirstEvent) {
invariant(
currentEvent.state === State.BEGAN,
errorMsgWithCurrentEvent('first event must have BEGAN state')
);
}
if (previousEvent !== null) {
if (previousEvent.state !== currentEvent.state) {
invariant(
hasProperty(currentEvent, 'oldState'),
errorMsgWithCurrentEvent(
'when state changes, oldState field should be present'
)
);
invariant(
currentEvent.oldState === previousEvent.state,
errorMsgWithBothEvents(
"when state changes, oldState should be the same as previous event' state"
)
);
}
}
return currentEvent;
}
type EventWithoutStates = Omit<GestureHandlerTestEvent, 'oldState' | 'state'>;
interface HandlerInfo {
handlerType: HandlerNames;
handlerTag: number;
}
function fillMissingDefaultsFor({
handlerType,
handlerTag,
}: HandlerInfo): (
event: Partial<GestureHandlerTestEvent>
) => EventWithoutStates {
return (event) => {
return {
...handlersDefaultEvents[handlerType],
...event,
handlerTag,
};
};
}
function isDiscreteHandler(handlerType: HandlerNames) {
return (
handlerType === 'TapGestureHandler' ||
handlerType === 'LongPressGestureHandler'
);
}
function fillMissingStatesTransitions(
events: EventWithoutStates[],
isDiscreteHandler: boolean
): EventWithoutStates[] {
type Event = EventWithoutStates | null;
const _events = [...events];
const lastEvent = _events[_events.length - 1] ?? null;
const firstEvent = _events[0] ?? null;
const shouldDuplicateFirstEvent =
!isDiscreteHandler && !hasState(State.BEGAN)(firstEvent);
if (shouldDuplicateFirstEvent) {
const duplicated = { ...firstEvent, state: State.BEGAN };
// @ts-ignore badly typed, property may exist and we don't want to copy it
delete duplicated.oldState;
_events.unshift(duplicated);
}
const shouldDuplicateLastEvent =
!hasState(State.END)(lastEvent) ||
!hasState(State.FAILED)(lastEvent) ||
!hasState(State.CANCELLED)(lastEvent);
if (shouldDuplicateLastEvent) {
const duplicated = { ...lastEvent, state: State.END };
// @ts-ignore badly typed, property may exist and we don't want to copy it
delete duplicated.oldState;
_events.push(duplicated);
}
function isWithoutState(event: Event) {
return event !== null && !hasProperty(event, 'state');
}
function hasState(state: State) {
return (event: Event) => event !== null && event.state === state;
}
function noEventsLeft(event: Event) {
return event === null;
}
function trueFn() {
return true;
}
interface Args {
shouldConsumeEvent?: (event: Event) => boolean;
shouldTransitionToNextState?: (nextEvent: Event) => boolean;
}
function fillEventsForCurrentState({
shouldConsumeEvent = trueFn,
shouldTransitionToNextState = trueFn,
}: Args) {
function peekCurrentEvent(): Event {
return _events[0] ?? null;
}
function peekNextEvent(): Event {
return _events[1] ?? null;
}
function consumeCurrentEvent() {
_events.shift();
}
const currentEvent = peekCurrentEvent();
const nextEvent = peekNextEvent();
const currentRequiredState = REQUIRED_EVENTS[currentStateIdx];
let eventData = {};
const shouldUseEvent = shouldConsumeEvent(currentEvent);
if (shouldUseEvent) {
eventData = currentEvent!;
consumeCurrentEvent();
}
transformedEvents.push({ state: currentRequiredState, ...eventData });
if (shouldTransitionToNextState(nextEvent)) {
currentStateIdx++;
}
}
const REQUIRED_EVENTS = [State.BEGAN, State.ACTIVE, State.END];
let currentStateIdx = 0;
const transformedEvents: EventWithoutStates[] = [];
let hasAllStates;
let iterations = 0;
do {
const nextRequiredState = REQUIRED_EVENTS[currentStateIdx];
if (nextRequiredState === State.BEGAN) {
fillEventsForCurrentState({
shouldConsumeEvent: (e: Event) =>
isWithoutState(e) || hasState(State.BEGAN)(e),
});
} else if (nextRequiredState === State.ACTIVE) {
const shouldConsumeEvent = (e: Event) =>
isWithoutState(e) || hasState(State.ACTIVE)(e);
const shouldTransitionToNextState = (nextEvent: Event) =>
noEventsLeft(nextEvent) ||
hasState(State.END)(nextEvent) ||
hasState(State.FAILED)(nextEvent) ||
hasState(State.CANCELLED)(nextEvent);
fillEventsForCurrentState({
shouldConsumeEvent,
shouldTransitionToNextState,
});
} else if (nextRequiredState === State.END) {
fillEventsForCurrentState({});
}
hasAllStates = currentStateIdx === REQUIRED_EVENTS.length;
invariant(
iterations++ <= 500,
'exceeded max number of iterations, please report a bug in RNGH repository with your test case'
);
} while (!hasAllStates);
return transformedEvents;
}
type EventEmitter = (
eventName: string,
args: { nativeEvent: GestureHandlerTestEvent }
) => void;
interface HandlerData {
emitEvent: EventEmitter;
handlerType: HandlerNames;
handlerTag: number;
}
function getHandlerData(
componentOrGesture: ReactTestInstance | GestureType
): HandlerData {
if (isGesture(componentOrGesture)) {
const gesture = componentOrGesture;
return {
emitEvent: (eventName, args) => {
DeviceEventEmitter.emit(eventName, args.nativeEvent);
},
handlerType: gesture.handlerName as HandlerNames,
handlerTag: gesture.handlerTag,
};
}
const gestureHandlerComponent = componentOrGesture;
return {
emitEvent: (eventName, args) => {
fireEvent(gestureHandlerComponent, eventName, args);
},
handlerType: gestureHandlerComponent.props.handlerType as HandlerNames,
handlerTag: gestureHandlerComponent.props.handlerTag as number,
};
}
type AllGestures =
| TapGesture
| PanGesture
| LongPressGesture
| RotationGesture
| PinchGesture
| FlingGesture
| ForceTouchGesture
| NativeGesture;
type AllHandlers =
| TapGestureHandler
| PanGestureHandler
| LongPressGestureHandler
| RotationGestureHandler
| PinchGestureHandler
| FlingGestureHandler
| ForceTouchGestureHandler
| NativeViewGestureHandler;
// prettier-ignore
type ClassComponentConstructor<P> = new (props: P) => React.Component<P, any, any>;
type ExtractPayloadFromProps<T> = T extends BaseGestureHandlerProps<
infer TPayload
>
? TPayload
: never;
type ExtractConfig<T> = T extends BaseGesture<infer TGesturePayload>
? TGesturePayload
: T extends ClassComponentConstructor<infer THandlerProps>
? ExtractPayloadFromProps<THandlerProps>
: Record<string, unknown>;
export function fireGestureHandler<THandler extends AllGestures | AllHandlers>(
componentOrGesture: ReactTestInstance | GestureType,
eventList: Partial<GestureHandlerTestEvent<ExtractConfig<THandler>>>[] = []
): void {
const { emitEvent, handlerType, handlerTag } =
getHandlerData(componentOrGesture);
let _ = fillMissingStatesTransitions(
eventList,
isDiscreteHandler(handlerType)
);
_ = _.map(fillMissingDefaultsFor({ handlerTag, handlerType }));
_ = withPrevAndCurrent(_, fillOldStateChanges);
_ = withPrevAndCurrent(_, validateStateTransitions);
// @ts-ignore TODO
_ = _.map(wrapWithNativeEvent);
const events = _ as unknown as WrappedGestureHandlerTestEvent[];
const firstEvent = events.shift()!;
emitEvent('onGestureHandlerStateChange', firstEvent);
let lastSentEvent = firstEvent;
for (const event of events) {
const hasChangedState =
lastSentEvent.nativeEvent.state !== event.nativeEvent.state;
if (hasChangedState) {
emitEvent('onGestureHandlerStateChange', event);
} else {
emitEvent('onGestureHandlerEvent', event);
}
lastSentEvent = event;
}
}
export function getByGestureTestId(testID: string) {
const handler = findHandlerByTestID(testID);
if (handler === null) {
throw new Error(`Handler with id: '${testID}' cannot be found`);
}
return handler;
}

View File

@@ -0,0 +1,69 @@
import {
TouchableHighlight,
TouchableNativeFeedback,
TouchableOpacity,
TouchableWithoutFeedback,
ScrollView,
FlatList,
Switch,
TextInput,
DrawerLayoutAndroid,
View,
} from 'react-native';
import { State } from './State';
import { Directions } from './Directions';
const NOOP = () => {
// do nothing
};
const PanGestureHandler = View;
const attachGestureHandler = NOOP;
const createGestureHandler = NOOP;
const dropGestureHandler = NOOP;
const updateGestureHandler = NOOP;
const flushOperations = NOOP;
const install = NOOP;
const NativeViewGestureHandler = View;
const TapGestureHandler = View;
const ForceTouchGestureHandler = View;
const LongPressGestureHandler = View;
const PinchGestureHandler = View;
const RotationGestureHandler = View;
const FlingGestureHandler = View;
const RawButton = TouchableNativeFeedback;
const BaseButton = TouchableNativeFeedback;
const RectButton = TouchableNativeFeedback;
const BorderlessButton = TouchableNativeFeedback;
export default {
TouchableHighlight,
TouchableNativeFeedback,
TouchableOpacity,
TouchableWithoutFeedback,
ScrollView,
FlatList,
Switch,
TextInput,
DrawerLayoutAndroid,
NativeViewGestureHandler,
TapGestureHandler,
ForceTouchGestureHandler,
LongPressGestureHandler,
PinchGestureHandler,
RotationGestureHandler,
FlingGestureHandler,
RawButton,
BaseButton,
RectButton,
BorderlessButton,
PanGestureHandler,
attachGestureHandler,
createGestureHandler,
dropGestureHandler,
updateGestureHandler,
flushOperations,
install,
// probably can be removed
Directions,
State,
} as const;

View File

@@ -0,0 +1,26 @@
import { TurboModuleRegistry, TurboModule } from 'react-native';
import { Double } from 'react-native/Libraries/Types/CodegenTypes';
export interface Spec extends TurboModule {
handleSetJSResponder: (tag: Double, blockNativeResponder: boolean) => void;
handleClearJSResponder: () => void;
createGestureHandler: (
handlerName: string,
handlerTag: Double,
// Record<> is not supported by codegen
// eslint-disable-next-line @typescript-eslint/ban-types
config: Object
) => void;
attachGestureHandler: (
handlerTag: Double,
newView: Double,
actionType: Double
) => void;
// eslint-disable-next-line @typescript-eslint/ban-types
updateGestureHandler: (handlerTag: Double, newConfig: Object) => void;
dropGestureHandler: (handlerTag: Double) => void;
install: () => boolean;
flushOperations: () => void;
}
export default TurboModuleRegistry.getEnforcing<Spec>('RNGestureHandlerModule');

View File

@@ -0,0 +1,22 @@
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
import type {
Int32,
WithDefault,
Float,
} from 'react-native/Libraries/Types/CodegenTypes';
import type { ViewProps, ColorValue } from 'react-native';
interface NativeProps extends ViewProps {
exclusive?: WithDefault<boolean, true>;
foreground?: boolean;
borderless?: boolean;
enabled?: WithDefault<boolean, true>;
rippleColor?: ColorValue;
rippleRadius?: Int32;
touchSoundDisabled?: WithDefault<boolean, false>;
borderWidth?: Float;
borderColor?: ColorValue;
borderStyle?: WithDefault<string, 'solid'>;
}
export default codegenNativeComponent<NativeProps>('RNGestureHandlerButton');

View File

@@ -0,0 +1,6 @@
import codegenNativeComponent from 'react-native/Libraries/Utilities/codegenNativeComponent';
import type { ViewProps } from 'react-native';
interface NativeProps extends ViewProps {}
export default codegenNativeComponent<NativeProps>('RNGestureHandlerRootView');

View File

@@ -0,0 +1 @@
export type ValueOf<T> = T[keyof T];

View File

@@ -0,0 +1,61 @@
export function toArray<T>(object: T | T[]): T[] {
if (!Array.isArray(object)) {
return [object];
}
return object;
}
export type withPrevAndCurrentMapFn<T, Transformed> = (
previous: Transformed | null,
current: T
) => Transformed;
export function withPrevAndCurrent<T, Transformed>(
array: T[],
mapFn: withPrevAndCurrentMapFn<T, Transformed>
): Transformed[] {
const previousArr: (null | Transformed)[] = [null];
const currentArr = [...array];
const transformedArr: Transformed[] = [];
currentArr.forEach((current, i) => {
// This type cast is fine and solves problem mentioned in https://github.com/software-mansion/react-native-gesture-handler/pull/2867 (namely that `previous` can be undefined).
// Unfortunately, linter on our CI does not allow this type of casting as it is unnecessary. To bypass that we use eslint-disable.
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
const previous = previousArr[i] as Transformed | null;
const transformed = mapFn(previous, current);
previousArr.push(transformed);
transformedArr.push(transformed);
});
return transformedArr;
}
// eslint-disable-next-line @typescript-eslint/ban-types
export function hasProperty(object: object, key: string) {
return Object.prototype.hasOwnProperty.call(object, key);
}
export function isJestEnv(): boolean {
// @ts-ignore Do not use `@types/node` because it will prioritise Node types over RN types which breaks the types (ex. setTimeout) in React Native projects.
return hasProperty(global, 'process') && !!process.env.JEST_WORKER_ID;
}
export function tagMessage(msg: string) {
return `[react-native-gesture-handler] ${msg}`;
}
// helper method to check whether Fabric is enabled, however global.nativeFabricUIManager
// may not be initialized before the first render
export function isFabric(): boolean {
// @ts-expect-error nativeFabricUIManager is not yet included in the RN types
return !!global?.nativeFabricUIManager;
}
export function isRemoteDebuggingEnabled(): boolean {
// react-native-reanimated checks if in remote debugging in the same way
// @ts-ignore global is available but node types are not included
const localGlobal = global as any;
return (
(!localGlobal.nativeCallSyncHook || !!localGlobal.__REMOTEDEV__) &&
!localGlobal.RN$Bridgeless
);
}

View File

@@ -0,0 +1,41 @@
// Gesture Handlers
import PanGestureHandler from './handlers/PanGestureHandler';
import TapGestureHandler from './handlers/TapGestureHandler';
import LongPressGestureHandler from './handlers/LongPressGestureHandler';
import PinchGestureHandler from './handlers/PinchGestureHandler';
import RotationGestureHandler from './handlers/RotationGestureHandler';
import FlingGestureHandler from './handlers/FlingGestureHandler';
import NativeViewGestureHandler from './handlers/NativeViewGestureHandler';
import ManualGestureHandler from './handlers/ManualGestureHandler';
import HoverGestureHandler from './handlers/HoverGestureHandler';
//Hammer Handlers
import HammerNativeViewGestureHandler from '../web_hammer/NativeViewGestureHandler';
import HammerPanGestureHandler from '../web_hammer/PanGestureHandler';
import HammerTapGestureHandler from '../web_hammer/TapGestureHandler';
import HammerLongPressGestureHandler from '../web_hammer/LongPressGestureHandler';
import HammerPinchGestureHandler from '../web_hammer/PinchGestureHandler';
import HammerRotationGestureHandler from '../web_hammer/RotationGestureHandler';
import HammerFlingGestureHandler from '../web_hammer/FlingGestureHandler';
export const Gestures = {
NativeViewGestureHandler,
PanGestureHandler,
TapGestureHandler,
LongPressGestureHandler,
PinchGestureHandler,
RotationGestureHandler,
FlingGestureHandler,
ManualGestureHandler,
HoverGestureHandler,
};
export const HammerGestures = {
NativeViewGestureHandler: HammerNativeViewGestureHandler,
PanGestureHandler: HammerPanGestureHandler,
TapGestureHandler: HammerTapGestureHandler,
LongPressGestureHandler: HammerLongPressGestureHandler,
PinchGestureHandler: HammerPinchGestureHandler,
RotationGestureHandler: HammerRotationGestureHandler,
FlingGestureHandler: HammerFlingGestureHandler,
};

View File

@@ -0,0 +1,2 @@
export const DEFAULT_TOUCH_SLOP = 15;
export const MINIMAL_FLING_VELOCITY = 0.1;

View File

@@ -0,0 +1,168 @@
import { AdaptedEvent, EventTypes } from '../interfaces';
import PointerTracker from '../tools/PointerTracker';
export interface RotationGestureListener {
onRotationBegin: (detector: RotationGestureDetector) => boolean;
onRotation: (detector: RotationGestureDetector) => boolean;
onRotationEnd: (detector: RotationGestureDetector) => void;
}
export default class RotationGestureDetector
implements RotationGestureListener
{
onRotationBegin: (detector: RotationGestureDetector) => boolean;
onRotation: (detector: RotationGestureDetector) => boolean;
onRotationEnd: (detector: RotationGestureDetector) => void;
private currentTime = 0;
private previousTime = 0;
private previousAngle = 0;
private rotation = 0;
private anchorX = 0;
private anchorY = 0;
private isInProgress = false;
private keyPointers: number[] = [NaN, NaN];
constructor(callbacks: RotationGestureListener) {
this.onRotationBegin = callbacks.onRotationBegin;
this.onRotation = callbacks.onRotation;
this.onRotationEnd = callbacks.onRotationEnd;
}
private updateCurrent(event: AdaptedEvent, tracker: PointerTracker): void {
this.previousTime = this.currentTime;
this.currentTime = event.time;
const [firstPointerID, secondPointerID] = this.keyPointers;
const firstPointerX: number = tracker.getLastX(firstPointerID);
const firstPointerY: number = tracker.getLastY(firstPointerID);
const secondPointerX: number = tracker.getLastX(secondPointerID);
const secondPointerY: number = tracker.getLastY(secondPointerID);
const vectorX: number = secondPointerX - firstPointerX;
const vectorY: number = secondPointerY - firstPointerY;
this.anchorX = (firstPointerX + secondPointerX) / 2;
this.anchorY = (firstPointerY + secondPointerY) / 2;
//Angle diff should be positive when rotating in clockwise direction
const angle: number = -Math.atan2(vectorY, vectorX);
this.rotation = Number.isNaN(this.previousAngle)
? 0
: this.previousAngle - angle;
this.previousAngle = angle;
if (this.rotation > Math.PI) {
this.rotation -= Math.PI;
} else if (this.rotation < -Math.PI) {
this.rotation += Math.PI;
}
if (this.rotation > Math.PI / 2) {
this.rotation -= Math.PI;
} else if (this.rotation < -Math.PI / 2) {
this.rotation += Math.PI;
}
}
private finish(): void {
if (!this.isInProgress) {
return;
}
this.isInProgress = false;
this.keyPointers = [NaN, NaN];
this.onRotationEnd(this);
}
private setKeyPointers(tracker: PointerTracker): void {
if (this.keyPointers[0] && this.keyPointers[1]) {
return;
}
const pointerIDs: IterableIterator<number> = tracker.getData().keys();
this.keyPointers[0] = pointerIDs.next().value as number;
this.keyPointers[1] = pointerIDs.next().value as number;
}
public onTouchEvent(event: AdaptedEvent, tracker: PointerTracker): boolean {
switch (event.eventType) {
case EventTypes.DOWN:
this.isInProgress = false;
break;
case EventTypes.ADDITIONAL_POINTER_DOWN:
if (this.isInProgress) {
break;
}
this.isInProgress = true;
this.previousTime = event.time;
this.previousAngle = NaN;
this.setKeyPointers(tracker);
this.updateCurrent(event, tracker);
this.onRotationBegin(this);
break;
case EventTypes.MOVE:
if (!this.isInProgress) {
break;
}
this.updateCurrent(event, tracker);
this.onRotation(this);
break;
case EventTypes.ADDITIONAL_POINTER_UP:
if (!this.isInProgress) {
break;
}
if (this.keyPointers.indexOf(event.pointerId) >= 0) {
this.finish();
}
break;
case EventTypes.UP:
if (this.isInProgress) {
this.finish();
}
break;
}
return true;
}
public getTimeDelta(): number {
return this.currentTime + this.previousTime;
}
public getAnchorX(): number {
return this.anchorX;
}
public getAnchorY(): number {
return this.anchorY;
}
public getRotation(): number {
return this.rotation;
}
public reset(): void {
this.keyPointers = [NaN, NaN];
this.isInProgress = false;
}
}

View File

@@ -0,0 +1,172 @@
import { DEFAULT_TOUCH_SLOP } from '../constants';
import { AdaptedEvent, EventTypes } from '../interfaces';
import PointerTracker from '../tools/PointerTracker';
export interface ScaleGestureListener {
onScaleBegin: (detector: ScaleGestureDetector) => boolean;
onScale: (detector: ScaleGestureDetector) => boolean;
onScaleEnd: (detector: ScaleGestureDetector) => void;
}
export default class ScaleGestureDetector implements ScaleGestureListener {
public onScaleBegin: (detector: ScaleGestureDetector) => boolean;
public onScale: (detector: ScaleGestureDetector) => boolean;
public onScaleEnd: (detector: ScaleGestureDetector) => void;
private focusX!: number;
private focusY!: number;
private currentSpan!: number;
private prevSpan!: number;
private initialSpan!: number;
private currentTime!: number;
private prevTime!: number;
private inProgress = false;
private spanSlop: number;
private minSpan: number;
public constructor(callbacks: ScaleGestureListener) {
this.onScaleBegin = callbacks.onScaleBegin;
this.onScale = callbacks.onScale;
this.onScaleEnd = callbacks.onScaleEnd;
this.spanSlop = DEFAULT_TOUCH_SLOP * 2;
this.minSpan = 0;
}
public onTouchEvent(event: AdaptedEvent, tracker: PointerTracker): boolean {
this.currentTime = event.time;
const action: EventTypes = event.eventType;
const numOfPointers = tracker.getTrackedPointersCount();
const streamComplete: boolean =
action === EventTypes.UP ||
action === EventTypes.ADDITIONAL_POINTER_UP ||
action === EventTypes.CANCEL;
if (action === EventTypes.DOWN || streamComplete) {
if (this.inProgress) {
this.onScaleEnd(this);
this.inProgress = false;
this.initialSpan = 0;
}
if (streamComplete) {
return true;
}
}
const configChanged: boolean =
action === EventTypes.DOWN ||
action === EventTypes.ADDITIONAL_POINTER_UP ||
action === EventTypes.ADDITIONAL_POINTER_DOWN;
const pointerUp = action === EventTypes.ADDITIONAL_POINTER_UP;
const ignoredPointer: number | undefined = pointerUp
? event.pointerId
: undefined;
//Determine focal point
const div: number = pointerUp ? numOfPointers - 1 : numOfPointers;
const sumX = tracker.getSumX(ignoredPointer);
const sumY = tracker.getSumY(ignoredPointer);
const focusX = sumX / div;
const focusY = sumY / div;
//Determine average deviation from focal point
let devSumX = 0;
let devSumY = 0;
tracker.getData().forEach((value, key) => {
if (key === ignoredPointer) {
return;
}
devSumX += Math.abs(value.lastX - focusX);
devSumY += Math.abs(value.lastY - focusY);
});
const devX: number = devSumX / div;
const devY: number = devSumY / div;
const spanX: number = devX * 2;
const spanY: number = devY * 2;
const span = Math.hypot(spanX, spanY);
//Begin/end events
const wasInProgress: boolean = this.inProgress;
this.focusX = focusX;
this.focusY = focusY;
if (this.inProgress && (span < this.minSpan || configChanged)) {
this.onScaleEnd(this);
this.inProgress = false;
this.initialSpan = span;
}
if (configChanged) {
this.initialSpan = this.prevSpan = this.currentSpan = span;
}
if (
!this.inProgress &&
span >= this.minSpan &&
(wasInProgress || Math.abs(span - this.initialSpan) > this.spanSlop)
) {
this.prevSpan = this.currentSpan = span;
this.prevTime = this.currentTime;
this.inProgress = this.onScaleBegin(this);
}
//Handle motion
if (action !== EventTypes.MOVE) {
return true;
}
this.currentSpan = span;
if (this.inProgress && !this.onScale(this)) {
return true;
}
this.prevSpan = this.currentSpan;
this.prevTime = this.currentTime;
return true;
}
public getCurrentSpan(): number {
return this.currentSpan;
}
public getFocusX(): number {
return this.focusX;
}
public getFocusY(): number {
return this.focusY;
}
public getTimeDelta(): number {
return this.currentTime - this.prevTime;
}
public getScaleFactor(numOfPointers: number): number {
if (numOfPointers < 2) {
return 1;
}
return this.prevSpan > 0 ? this.currentSpan / this.prevSpan : 1;
}
}

View File

@@ -0,0 +1,195 @@
import { State } from '../../State';
import { DiagonalDirections, Directions } from '../../Directions';
import { AdaptedEvent, Config } from '../interfaces';
import GestureHandler from './GestureHandler';
import Vector from '../tools/Vector';
import { coneToDeviation } from '../utils';
const DEFAULT_MAX_DURATION_MS = 800;
const DEFAULT_MIN_VELOCITY = 700;
const DEFAULT_ALIGNMENT_CONE = 30;
const DEFAULT_DIRECTION = Directions.RIGHT;
const DEFAULT_NUMBER_OF_TOUCHES_REQUIRED = 1;
const AXIAL_DEVIATION_COSINE = coneToDeviation(DEFAULT_ALIGNMENT_CONE);
const DIAGONAL_DEVIATION_COSINE = coneToDeviation(90 - DEFAULT_ALIGNMENT_CONE);
export default class FlingGestureHandler extends GestureHandler {
private numberOfPointersRequired = DEFAULT_NUMBER_OF_TOUCHES_REQUIRED;
private direction: Directions = DEFAULT_DIRECTION;
private maxDurationMs = DEFAULT_MAX_DURATION_MS;
private minVelocity = DEFAULT_MIN_VELOCITY;
private delayTimeout!: number;
private maxNumberOfPointersSimultaneously = 0;
private keyPointer = NaN;
public init(ref: number, propsRef: React.RefObject<unknown>): void {
super.init(ref, propsRef);
}
public updateGestureConfig({ enabled = true, ...props }: Config): void {
super.updateGestureConfig({ enabled: enabled, ...props });
if (this.config.direction) {
this.direction = this.config.direction;
}
if (this.config.numberOfPointers) {
this.numberOfPointersRequired = this.config.numberOfPointers;
}
}
private startFling(): void {
this.begin();
this.maxNumberOfPointersSimultaneously = 1;
this.delayTimeout = setTimeout(() => this.fail(), this.maxDurationMs);
}
private tryEndFling(): boolean {
const velocityVector = Vector.fromVelocity(this.tracker, this.keyPointer);
const getAlignment = (
direction: Directions | DiagonalDirections,
minimalAlignmentCosine: number
) => {
return (
(direction & this.direction) === direction &&
velocityVector.isSimilar(
Vector.fromDirection(direction),
minimalAlignmentCosine
)
);
};
const axialDirectionsList = Object.values(Directions);
const diagonalDirectionsList = Object.values(DiagonalDirections);
// list of alignments to all activated directions
const axialAlignmentList = axialDirectionsList.map((direction) =>
getAlignment(direction, AXIAL_DEVIATION_COSINE)
);
const diagonalAlignmentList = diagonalDirectionsList.map((direction) =>
getAlignment(direction, DIAGONAL_DEVIATION_COSINE)
);
const isAligned =
axialAlignmentList.some(Boolean) || diagonalAlignmentList.some(Boolean);
const isFast = velocityVector.magnitude > this.minVelocity;
if (
this.maxNumberOfPointersSimultaneously ===
this.numberOfPointersRequired &&
isAligned &&
isFast
) {
clearTimeout(this.delayTimeout);
this.activate();
return true;
}
return false;
}
private endFling() {
if (!this.tryEndFling()) {
this.fail();
}
}
protected onPointerDown(event: AdaptedEvent): void {
if (!this.isButtonInConfig(event.button)) {
return;
}
this.tracker.addToTracker(event);
this.keyPointer = event.pointerId;
super.onPointerDown(event);
this.newPointerAction();
}
protected onPointerAdd(event: AdaptedEvent): void {
this.tracker.addToTracker(event);
super.onPointerAdd(event);
this.newPointerAction();
}
private newPointerAction(): void {
if (this.currentState === State.UNDETERMINED) {
this.startFling();
}
if (this.currentState !== State.BEGAN) {
return;
}
this.tryEndFling();
if (
this.tracker.getTrackedPointersCount() >
this.maxNumberOfPointersSimultaneously
) {
this.maxNumberOfPointersSimultaneously =
this.tracker.getTrackedPointersCount();
}
}
private pointerMoveAction(event: AdaptedEvent): void {
this.tracker.track(event);
if (this.currentState !== State.BEGAN) {
return;
}
this.tryEndFling();
}
protected onPointerMove(event: AdaptedEvent): void {
this.pointerMoveAction(event);
super.onPointerMove(event);
}
protected onPointerOutOfBounds(event: AdaptedEvent): void {
this.pointerMoveAction(event);
super.onPointerOutOfBounds(event);
}
protected onPointerUp(event: AdaptedEvent): void {
super.onPointerUp(event);
this.onUp(event);
this.keyPointer = NaN;
}
protected onPointerRemove(event: AdaptedEvent): void {
super.onPointerRemove(event);
this.onUp(event);
}
private onUp(event: AdaptedEvent): void {
if (this.currentState === State.BEGAN) {
this.endFling();
}
this.tracker.removeFromTracker(event.pointerId);
}
public activate(force?: boolean): void {
super.activate(force);
this.end();
}
protected resetConfig(): void {
super.resetConfig();
this.numberOfPointersRequired = DEFAULT_NUMBER_OF_TOUCHES_REQUIRED;
this.direction = DEFAULT_DIRECTION;
}
}

View File

@@ -0,0 +1,871 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import { State } from '../../State';
import {
Config,
AdaptedEvent,
PropsRef,
ResultEvent,
PointerData,
ResultTouchEvent,
TouchEventType,
EventTypes,
} from '../interfaces';
import EventManager from '../tools/EventManager';
import GestureHandlerOrchestrator from '../tools/GestureHandlerOrchestrator';
import InteractionManager from '../tools/InteractionManager';
import PointerTracker, { TrackerElement } from '../tools/PointerTracker';
import { GestureHandlerDelegate } from '../tools/GestureHandlerDelegate';
import IGestureHandler from './IGestureHandler';
import { MouseButton } from '../../handlers/gestureHandlerCommon';
import { PointerType } from '../../PointerType';
export default abstract class GestureHandler implements IGestureHandler {
private lastSentState: State | null = null;
protected currentState: State = State.UNDETERMINED;
protected shouldCancelWhenOutside = false;
protected hasCustomActivationCriteria = false;
protected enabled = false;
private viewRef!: number;
private propsRef!: React.RefObject<unknown>;
private handlerTag!: number;
protected config: Config = { enabled: false };
protected tracker: PointerTracker = new PointerTracker();
// Orchestrator properties
protected activationIndex = 0;
protected awaiting = false;
protected active = false;
protected shouldResetProgress = false;
protected pointerType: PointerType = PointerType.MOUSE;
protected delegate: GestureHandlerDelegate<unknown, IGestureHandler>;
public constructor(
delegate: GestureHandlerDelegate<unknown, IGestureHandler>
) {
this.delegate = delegate;
}
//
// Initializing handler
//
protected init(viewRef: number, propsRef: React.RefObject<unknown>) {
this.propsRef = propsRef;
this.viewRef = viewRef;
this.currentState = State.UNDETERMINED;
this.delegate.init(viewRef, this);
}
public attachEventManager(manager: EventManager<unknown>): void {
manager.setOnPointerDown(this.onPointerDown.bind(this));
manager.setOnPointerAdd(this.onPointerAdd.bind(this));
manager.setOnPointerUp(this.onPointerUp.bind(this));
manager.setOnPointerRemove(this.onPointerRemove.bind(this));
manager.setOnPointerMove(this.onPointerMove.bind(this));
manager.setOnPointerEnter(this.onPointerEnter.bind(this));
manager.setOnPointerLeave(this.onPointerLeave.bind(this));
manager.setOnPointerCancel(this.onPointerCancel.bind(this));
manager.setOnPointerOutOfBounds(this.onPointerOutOfBounds.bind(this));
manager.setOnPointerMoveOver(this.onPointerMoveOver.bind(this));
manager.setOnPointerMoveOut(this.onPointerMoveOut.bind(this));
manager.registerListeners();
}
//
// Resetting handler
//
protected onCancel(): void {}
protected onReset(): void {}
protected resetProgress(): void {}
public reset(): void {
this.tracker.resetTracker();
this.onReset();
this.resetProgress();
this.delegate.reset();
this.currentState = State.UNDETERMINED;
}
//
// State logic
//
public moveToState(newState: State, sendIfDisabled?: boolean) {
if (this.currentState === newState) {
return;
}
const oldState = this.currentState;
this.currentState = newState;
if (
this.tracker.getTrackedPointersCount() > 0 &&
this.config.needsPointerData &&
this.isFinished()
) {
this.cancelTouches();
}
GestureHandlerOrchestrator.getInstance().onHandlerStateChange(
this,
newState,
oldState,
sendIfDisabled
);
this.onStateChange(newState, oldState);
if (!this.enabled && this.isFinished()) {
this.currentState = State.UNDETERMINED;
}
}
protected onStateChange(_newState: State, _oldState: State): void {}
public begin(): void {
if (!this.checkHitSlop()) {
return;
}
if (this.currentState === State.UNDETERMINED) {
this.moveToState(State.BEGAN);
}
}
/**
* @param {boolean} sendIfDisabled - Used when handler becomes disabled. With this flag orchestrator will be forced to send fail event
*/
public fail(sendIfDisabled?: boolean): void {
if (
this.currentState === State.ACTIVE ||
this.currentState === State.BEGAN
) {
// Here the order of calling the delegate and moveToState is important.
// At this point we can use currentState as previuos state, because immediately after changing cursor we call moveToState method.
this.delegate.onFail();
this.moveToState(State.FAILED, sendIfDisabled);
}
this.resetProgress();
}
/**
* @param {boolean} sendIfDisabled - Used when handler becomes disabled. With this flag orchestrator will be forced to send cancel event
*/
public cancel(sendIfDisabled?: boolean): void {
if (
this.currentState === State.ACTIVE ||
this.currentState === State.UNDETERMINED ||
this.currentState === State.BEGAN
) {
this.onCancel();
// Same as above - order matters
this.delegate.onCancel();
this.moveToState(State.CANCELLED, sendIfDisabled);
}
}
public activate(force = false) {
if (
(this.config.manualActivation !== true || force) &&
(this.currentState === State.UNDETERMINED ||
this.currentState === State.BEGAN)
) {
this.delegate.onActivate();
this.moveToState(State.ACTIVE);
}
}
public end() {
if (
this.currentState === State.BEGAN ||
this.currentState === State.ACTIVE
) {
// Same as above - order matters
this.delegate.onEnd();
this.moveToState(State.END);
}
this.resetProgress();
}
//
// Methods for orchestrator
//
public isAwaiting(): boolean {
return this.awaiting;
}
public setAwaiting(value: boolean): void {
this.awaiting = value;
}
public isActive(): boolean {
return this.active;
}
public setActive(value: boolean): void {
this.active = value;
}
public getShouldResetProgress(): boolean {
return this.shouldResetProgress;
}
public setShouldResetProgress(value: boolean): void {
this.shouldResetProgress = value;
}
public getActivationIndex(): number {
return this.activationIndex;
}
public setActivationIndex(value: number): void {
this.activationIndex = value;
}
public shouldWaitForHandlerFailure(handler: IGestureHandler): boolean {
if (handler === this) {
return false;
}
return InteractionManager.getInstance().shouldWaitForHandlerFailure(
this,
handler
);
}
public shouldRequireToWaitForFailure(handler: IGestureHandler): boolean {
if (handler === this) {
return false;
}
return InteractionManager.getInstance().shouldRequireHandlerToWaitForFailure(
this,
handler
);
}
public shouldRecognizeSimultaneously(handler: IGestureHandler): boolean {
if (handler === this) {
return true;
}
return InteractionManager.getInstance().shouldRecognizeSimultaneously(
this,
handler
);
}
public shouldBeCancelledByOther(handler: IGestureHandler): boolean {
if (handler === this) {
return false;
}
return InteractionManager.getInstance().shouldHandlerBeCancelledBy(
this,
handler
);
}
//
// Event actions
//
protected onPointerDown(event: AdaptedEvent): void {
GestureHandlerOrchestrator.getInstance().recordHandlerIfNotPresent(this);
this.pointerType = event.pointerType;
if (this.pointerType === PointerType.TOUCH) {
GestureHandlerOrchestrator.getInstance().cancelMouseAndPenGestures(this);
}
if (this.config.needsPointerData) {
this.sendTouchEvent(event);
}
}
// Adding another pointer to existing ones
protected onPointerAdd(event: AdaptedEvent): void {
if (this.config.needsPointerData) {
this.sendTouchEvent(event);
}
}
protected onPointerUp(event: AdaptedEvent): void {
if (this.config.needsPointerData) {
this.sendTouchEvent(event);
}
}
// Removing pointer, when there is more than one pointers
protected onPointerRemove(event: AdaptedEvent): void {
if (this.config.needsPointerData) {
this.sendTouchEvent(event);
}
}
protected onPointerMove(event: AdaptedEvent): void {
this.tryToSendMoveEvent(false);
if (this.config.needsPointerData) {
this.sendTouchEvent(event);
}
}
protected onPointerLeave(event: AdaptedEvent): void {
if (this.shouldCancelWhenOutside) {
switch (this.currentState) {
case State.ACTIVE:
this.cancel();
break;
case State.BEGAN:
this.fail();
break;
}
return;
}
if (this.config.needsPointerData) {
this.sendTouchEvent(event);
}
}
protected onPointerEnter(event: AdaptedEvent): void {
if (this.config.needsPointerData) {
this.sendTouchEvent(event);
}
}
protected onPointerCancel(event: AdaptedEvent): void {
if (this.config.needsPointerData) {
this.sendTouchEvent(event);
}
this.cancel();
this.reset();
}
protected onPointerOutOfBounds(event: AdaptedEvent): void {
this.tryToSendMoveEvent(true);
if (this.config.needsPointerData) {
this.sendTouchEvent(event);
}
}
protected onPointerMoveOver(_event: AdaptedEvent): void {
// used only by hover gesture handler atm
}
protected onPointerMoveOut(_event: AdaptedEvent): void {
// used only by hover gesture handler atm
}
private tryToSendMoveEvent(out: boolean): void {
if (
this.enabled &&
this.active &&
(!out || (out && !this.shouldCancelWhenOutside))
) {
this.sendEvent(this.currentState, this.currentState);
}
}
public sendTouchEvent(event: AdaptedEvent): void {
if (!this.enabled) {
return;
}
const { onGestureHandlerEvent }: PropsRef = this.propsRef
.current as PropsRef;
const touchEvent: ResultTouchEvent | undefined =
this.transformTouchEvent(event);
if (touchEvent) {
invokeNullableMethod(onGestureHandlerEvent, touchEvent);
}
}
//
// Events Sending
//
public sendEvent = (newState: State, oldState: State): void => {
const { onGestureHandlerEvent, onGestureHandlerStateChange }: PropsRef =
this.propsRef.current as PropsRef;
const resultEvent: ResultEvent = this.transformEventData(
newState,
oldState
);
// In the new API oldState field has to be undefined, unless we send event state changed
// Here the order is flipped to avoid workarounds such as making backup of the state and setting it to undefined first, then changing it back
// Flipping order with setting oldState to undefined solves issue, when events were being sent twice instead of once
// However, this may cause trouble in the future (but for now we don't know that)
if (this.lastSentState !== newState) {
this.lastSentState = newState;
invokeNullableMethod(onGestureHandlerStateChange, resultEvent);
}
if (this.currentState === State.ACTIVE) {
resultEvent.nativeEvent.oldState = undefined;
invokeNullableMethod(onGestureHandlerEvent, resultEvent);
}
};
private transformEventData(newState: State, oldState: State): ResultEvent {
return {
nativeEvent: {
numberOfPointers: this.tracker.getTrackedPointersCount(),
state: newState,
pointerInside: this.delegate.isPointerInBounds({
x: this.tracker.getLastAvgX(),
y: this.tracker.getLastAvgY(),
}),
...this.transformNativeEvent(),
handlerTag: this.handlerTag,
target: this.viewRef,
oldState: newState !== oldState ? oldState : undefined,
pointerType: this.pointerType,
},
timeStamp: Date.now(),
};
}
private transformTouchEvent(
event: AdaptedEvent
): ResultTouchEvent | undefined {
const rect = this.delegate.measureView();
const all: PointerData[] = [];
const changed: PointerData[] = [];
const trackerData = this.tracker.getData();
// This if handles edge case where all pointers have been cancelled
// When pointercancel is triggered, reset method is called. This means that tracker will be reset after first pointer being cancelled
// The problem is, that handler will receive another pointercancel event from the rest of the pointers
// To avoid crashing, we don't send event if tracker tracks no pointers, i.e. has been reset
if (trackerData.size === 0 || !trackerData.has(event.pointerId)) {
return;
}
trackerData.forEach((element: TrackerElement, key: number): void => {
const id: number = this.tracker.getMappedTouchEventId(key);
all.push({
id: id,
x: element.lastX - rect.pageX,
y: element.lastY - rect.pageY,
absoluteX: element.lastX,
absoluteY: element.lastY,
});
});
// Each pointer sends its own event, so we want changed touches to contain only the pointer that has changed.
// However, if the event is cancel, we want to cancel all pointers to avoid crashes
if (event.eventType !== EventTypes.CANCEL) {
changed.push({
id: this.tracker.getMappedTouchEventId(event.pointerId),
x: event.x - rect.pageX,
y: event.y - rect.pageY,
absoluteX: event.x,
absoluteY: event.y,
});
} else {
trackerData.forEach((element: TrackerElement, key: number): void => {
const id: number = this.tracker.getMappedTouchEventId(key);
changed.push({
id: id,
x: element.lastX - rect.pageX,
y: element.lastY - rect.pageY,
absoluteX: element.lastX,
absoluteY: element.lastY,
});
});
}
let eventType: TouchEventType = TouchEventType.UNDETERMINED;
switch (event.eventType) {
case EventTypes.DOWN:
case EventTypes.ADDITIONAL_POINTER_DOWN:
eventType = TouchEventType.DOWN;
break;
case EventTypes.UP:
case EventTypes.ADDITIONAL_POINTER_UP:
eventType = TouchEventType.UP;
break;
case EventTypes.MOVE:
eventType = TouchEventType.MOVE;
break;
case EventTypes.CANCEL:
eventType = TouchEventType.CANCELLED;
break;
}
// Here, when we receive up event, we want to decrease number of touches
// That's because we want handler to send information that there's one pointer less
// However, we still want this pointer to be present in allTouches array, so that its data can be accessed
let numberOfTouches: number = all.length;
if (
event.eventType === EventTypes.UP ||
event.eventType === EventTypes.ADDITIONAL_POINTER_UP
) {
--numberOfTouches;
}
return {
nativeEvent: {
handlerTag: this.handlerTag,
state: this.currentState,
eventType: event.touchEventType ?? eventType,
changedTouches: changed,
allTouches: all,
numberOfTouches: numberOfTouches,
},
timeStamp: Date.now(),
};
}
private cancelTouches(): void {
const rect = this.delegate.measureView();
const all: PointerData[] = [];
const changed: PointerData[] = [];
const trackerData = this.tracker.getData();
if (trackerData.size === 0) {
return;
}
trackerData.forEach((element: TrackerElement, key: number): void => {
const id: number = this.tracker.getMappedTouchEventId(key);
all.push({
id: id,
x: element.lastX - rect.pageX,
y: element.lastY - rect.pageY,
absoluteX: element.lastX,
absoluteY: element.lastY,
});
changed.push({
id: id,
x: element.lastX - rect.pageX,
y: element.lastY - rect.pageY,
absoluteX: element.lastX,
absoluteY: element.lastY,
});
});
const cancelEvent: ResultTouchEvent = {
nativeEvent: {
handlerTag: this.handlerTag,
state: this.currentState,
eventType: TouchEventType.CANCELLED,
changedTouches: changed,
allTouches: all,
numberOfTouches: all.length,
},
timeStamp: Date.now(),
};
const { onGestureHandlerEvent }: PropsRef = this.propsRef
.current as PropsRef;
invokeNullableMethod(onGestureHandlerEvent, cancelEvent);
}
protected transformNativeEvent(): Record<string, unknown> {
// those properties are shared by most handlers and if not this method will be overriden
const rect = this.delegate.measureView();
return {
x: this.tracker.getLastAvgX() - rect.pageX,
y: this.tracker.getLastAvgY() - rect.pageY,
absoluteX: this.tracker.getLastAvgX(),
absoluteY: this.tracker.getLastAvgY(),
};
}
//
// Handling config
//
public updateGestureConfig({ enabled = true, ...props }: Config): void {
this.config = { enabled: enabled, ...props };
this.enabled = enabled;
if (this.config.shouldCancelWhenOutside !== undefined) {
this.setShouldCancelWhenOutside(this.config.shouldCancelWhenOutside);
}
this.validateHitSlops();
if (this.enabled) {
return;
}
switch (this.currentState) {
case State.ACTIVE:
this.fail(true);
break;
case State.UNDETERMINED:
GestureHandlerOrchestrator.getInstance().removeHandlerFromOrchestrator(
this
);
break;
default:
this.cancel(true);
break;
}
}
protected checkCustomActivationCriteria(criterias: string[]): void {
for (const key in this.config) {
if (criterias.indexOf(key) >= 0) {
this.hasCustomActivationCriteria = true;
}
}
}
private validateHitSlops(): void {
if (!this.config.hitSlop) {
return;
}
if (
this.config.hitSlop.left !== undefined &&
this.config.hitSlop.right !== undefined &&
this.config.hitSlop.width !== undefined
) {
throw new Error(
'HitSlop Error: Cannot define left, right and width at the same time'
);
}
if (
this.config.hitSlop.width !== undefined &&
this.config.hitSlop.left === undefined &&
this.config.hitSlop.right === undefined
) {
throw new Error(
'HitSlop Error: When width is defined, either left or right has to be defined'
);
}
if (
this.config.hitSlop.height !== undefined &&
this.config.hitSlop.top !== undefined &&
this.config.hitSlop.bottom !== undefined
) {
throw new Error(
'HitSlop Error: Cannot define top, bottom and height at the same time'
);
}
if (
this.config.hitSlop.height !== undefined &&
this.config.hitSlop.top === undefined &&
this.config.hitSlop.bottom === undefined
) {
throw new Error(
'HitSlop Error: When height is defined, either top or bottom has to be defined'
);
}
}
private checkHitSlop(): boolean {
if (!this.config.hitSlop) {
return true;
}
const { width, height } = this.delegate.measureView();
let left = 0;
let top = 0;
let right: number = width;
let bottom: number = height;
if (this.config.hitSlop.horizontal !== undefined) {
left -= this.config.hitSlop.horizontal;
right += this.config.hitSlop.horizontal;
}
if (this.config.hitSlop.vertical !== undefined) {
top -= this.config.hitSlop.vertical;
bottom += this.config.hitSlop.vertical;
}
if (this.config.hitSlop.left !== undefined) {
left = -this.config.hitSlop.left;
}
if (this.config.hitSlop.right !== undefined) {
right = width + this.config.hitSlop.right;
}
if (this.config.hitSlop.top !== undefined) {
top = -this.config.hitSlop.top;
}
if (this.config.hitSlop.bottom !== undefined) {
bottom = width + this.config.hitSlop.bottom;
}
if (this.config.hitSlop.width !== undefined) {
if (this.config.hitSlop.left !== undefined) {
right = left + this.config.hitSlop.width;
} else if (this.config.hitSlop.right !== undefined) {
left = right - this.config.hitSlop.width;
}
}
if (this.config.hitSlop.height !== undefined) {
if (this.config.hitSlop.top !== undefined) {
bottom = top + this.config.hitSlop.height;
} else if (this.config.hitSlop.bottom !== undefined) {
top = bottom - this.config.hitSlop.height;
}
}
const rect = this.delegate.measureView();
const offsetX: number = this.tracker.getLastX() - rect.pageX;
const offsetY: number = this.tracker.getLastY() - rect.pageY;
if (
offsetX >= left &&
offsetX <= right &&
offsetY >= top &&
offsetY <= bottom
) {
return true;
}
return false;
}
public isButtonInConfig(mouseButton: MouseButton | undefined) {
return (
!mouseButton ||
(!this.config.mouseButton && mouseButton === MouseButton.LEFT) ||
(this.config.mouseButton && mouseButton & this.config.mouseButton)
);
}
protected resetConfig(): void {}
public onDestroy(): void {
this.delegate.destroy(this.config);
}
//
// Getters and setters
//
public getTag(): number {
return this.handlerTag;
}
public setTag(tag: number): void {
this.handlerTag = tag;
}
public getConfig() {
return this.config;
}
public getDelegate(): GestureHandlerDelegate<unknown, IGestureHandler> {
return this.delegate;
}
public getTracker(): PointerTracker {
return this.tracker;
}
public getTrackedPointersID(): number[] {
return this.tracker.getTrackedPointersID();
}
public getState(): State {
return this.currentState;
}
public isEnabled(): boolean {
return this.enabled;
}
private isFinished(): boolean {
return (
this.currentState === State.END ||
this.currentState === State.FAILED ||
this.currentState === State.CANCELLED
);
}
protected setShouldCancelWhenOutside(shouldCancel: boolean) {
this.shouldCancelWhenOutside = shouldCancel;
}
protected getShouldCancelWhenOutside(): boolean {
return this.shouldCancelWhenOutside;
}
public getPointerType(): PointerType {
return this.pointerType;
}
}
function invokeNullableMethod(
method:
| ((event: ResultEvent | ResultTouchEvent) => void)
| { __getHandler: () => (event: ResultEvent | ResultTouchEvent) => void }
| { __nodeConfig: { argMapping: unknown[] } },
event: ResultEvent | ResultTouchEvent
): void {
if (!method) {
return;
}
if (typeof method === 'function') {
method(event);
return;
}
if ('__getHandler' in method && typeof method.__getHandler === 'function') {
const handler = method.__getHandler();
invokeNullableMethod(handler, event);
return;
}
if (!('__nodeConfig' in method)) {
return;
}
const { argMapping }: { argMapping: unknown } = method.__nodeConfig;
if (!Array.isArray(argMapping)) {
return;
}
for (const [index, [key, value]] of argMapping.entries()) {
if (!(key in event.nativeEvent)) {
continue;
}
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
const nativeValue = event.nativeEvent[key];
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
if (value?.setValue) {
//Reanimated API
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call
value.setValue(nativeValue);
} else {
//RN Animated API
method.__nodeConfig.argMapping[index] = [key, nativeValue];
}
}
return;
}

View File

@@ -0,0 +1,43 @@
import { State } from '../../State';
import { AdaptedEvent, Config } from '../interfaces';
import GestureHandlerOrchestrator from '../tools/GestureHandlerOrchestrator';
import GestureHandler from './GestureHandler';
export default class HoverGestureHandler extends GestureHandler {
public init(ref: number, propsRef: React.RefObject<unknown>) {
super.init(ref, propsRef);
}
public updateGestureConfig({ enabled = true, ...props }: Config): void {
super.updateGestureConfig({ enabled: enabled, ...props });
}
protected onPointerMoveOver(event: AdaptedEvent): void {
GestureHandlerOrchestrator.getInstance().recordHandlerIfNotPresent(this);
this.tracker.addToTracker(event);
super.onPointerMoveOver(event);
if (this.getState() === State.UNDETERMINED) {
this.begin();
this.activate();
}
}
protected onPointerMoveOut(event: AdaptedEvent): void {
this.tracker.addToTracker(event);
super.onPointerMoveOut(event);
this.end();
}
protected onPointerMove(event: AdaptedEvent): void {
this.tracker.track(event);
super.onPointerMove(event);
}
protected onPointerCancel(event: AdaptedEvent): void {
super.onPointerCancel(event);
this.reset();
}
}

View File

@@ -0,0 +1,50 @@
import type { PointerType } from '../../PointerType';
import type { MouseButton } from '../../handlers/gestureHandlerCommon';
import type { State } from '../../State';
import type { Config } from '../interfaces';
import type EventManager from '../tools/EventManager';
import type { GestureHandlerDelegate } from '../tools/GestureHandlerDelegate';
import type PointerTracker from '../tools/PointerTracker';
export default interface IGestureHandler {
getTag: () => number;
getState: () => State;
getConfig: () => Config;
getDelegate: () => GestureHandlerDelegate<unknown, this>;
attachEventManager: (manager: EventManager<unknown>) => void;
isButtonInConfig: (
mouseButton: MouseButton | undefined
) => boolean | number | undefined;
getPointerType: () => PointerType;
getTracker: () => PointerTracker;
getTrackedPointersID: () => number[];
begin: () => void;
activate: (force: boolean) => void;
end: () => void;
fail: () => void;
cancel: () => void;
reset: () => void;
isEnabled: () => boolean;
isActive: () => boolean;
setActive: (value: boolean) => void;
isAwaiting: () => boolean;
setAwaiting: (value: boolean) => void;
setActivationIndex: (value: number) => void;
setShouldResetProgress: (value: boolean) => void;
shouldWaitForHandlerFailure: (handler: IGestureHandler) => boolean;
shouldRequireToWaitForFailure: (handler: IGestureHandler) => boolean;
shouldRecognizeSimultaneously: (handler: IGestureHandler) => boolean;
shouldBeCancelledByOther: (handler: IGestureHandler) => boolean;
sendEvent: (newState: State, oldState: State) => void;
updateGestureConfig: (config: Config) => void;
isButton?: () => boolean;
}

View File

@@ -0,0 +1,128 @@
import { State } from '../../State';
import { AdaptedEvent, Config } from '../interfaces';
import GestureHandler from './GestureHandler';
const DEFAULT_MIN_DURATION_MS = 500;
const DEFAULT_MAX_DIST_DP = 10;
const SCALING_FACTOR = 10;
export default class LongPressGestureHandler extends GestureHandler {
private minDurationMs = DEFAULT_MIN_DURATION_MS;
private defaultMaxDistSq = DEFAULT_MAX_DIST_DP * SCALING_FACTOR;
private maxDistSq = this.defaultMaxDistSq;
private startX = 0;
private startY = 0;
private startTime = 0;
private previousTime = 0;
private activationTimeout: number | undefined;
public init(ref: number, propsRef: React.RefObject<unknown>) {
if (this.config.enableContextMenu === undefined) {
this.config.enableContextMenu = false;
}
super.init(ref, propsRef);
}
protected transformNativeEvent() {
return {
...super.transformNativeEvent(),
duration: Date.now() - this.startTime,
};
}
public updateGestureConfig({ enabled = true, ...props }: Config): void {
super.updateGestureConfig({ enabled: enabled, ...props });
if (this.config.minDurationMs !== undefined) {
this.minDurationMs = this.config.minDurationMs;
}
if (this.config.maxDist !== undefined) {
this.maxDistSq = this.config.maxDist * this.config.maxDist;
}
}
protected resetConfig(): void {
super.resetConfig();
this.minDurationMs = DEFAULT_MIN_DURATION_MS;
this.maxDistSq = this.defaultMaxDistSq;
}
protected onStateChange(_newState: State, _oldState: State): void {
clearTimeout(this.activationTimeout);
}
protected onPointerDown(event: AdaptedEvent): void {
if (!this.isButtonInConfig(event.button)) {
return;
}
this.tracker.addToTracker(event);
super.onPointerDown(event);
this.tryBegin(event);
this.tryActivate();
this.checkDistanceFail(event);
}
protected onPointerMove(event: AdaptedEvent): void {
super.onPointerMove(event);
this.tracker.track(event);
this.checkDistanceFail(event);
}
protected onPointerUp(event: AdaptedEvent): void {
super.onPointerUp(event);
this.tracker.removeFromTracker(event.pointerId);
if (this.currentState === State.ACTIVE) {
this.end();
} else {
this.fail();
}
}
private tryBegin(event: AdaptedEvent): void {
if (this.currentState !== State.UNDETERMINED) {
return;
}
this.previousTime = Date.now();
this.startTime = this.previousTime;
this.begin();
this.startX = event.x;
this.startY = event.y;
}
private tryActivate(): void {
if (this.minDurationMs > 0) {
this.activationTimeout = setTimeout(() => {
this.activate();
}, this.minDurationMs);
} else if (this.minDurationMs === 0) {
this.activate();
}
}
private checkDistanceFail(event: AdaptedEvent): void {
const dx = event.x - this.startX;
const dy = event.y - this.startY;
const distSq = dx * dx + dy * dy;
if (distSq <= this.maxDistSq) {
return;
}
if (this.currentState === State.ACTIVE) {
this.cancel();
} else {
this.fail();
}
}
}

View File

@@ -0,0 +1,43 @@
import { AdaptedEvent, Config } from '../interfaces';
import GestureHandler from './GestureHandler';
export default class ManualGestureHandler extends GestureHandler {
public init(ref: number, propsRef: React.RefObject<unknown>) {
super.init(ref, propsRef);
}
public updateGestureConfig({ enabled = true, ...props }: Config): void {
super.updateGestureConfig({ enabled: enabled, ...props });
}
protected onPointerDown(event: AdaptedEvent): void {
this.tracker.addToTracker(event);
super.onPointerDown(event);
this.begin();
}
protected onPointerAdd(event: AdaptedEvent): void {
this.tracker.addToTracker(event);
super.onPointerAdd(event);
}
protected onPointerMove(event: AdaptedEvent): void {
this.tracker.track(event);
super.onPointerMove(event);
}
protected onPointerOutOfBounds(event: AdaptedEvent): void {
this.tracker.track(event);
super.onPointerOutOfBounds(event);
}
protected onPointerUp(event: AdaptedEvent): void {
super.onPointerUp(event);
this.tracker.removeFromTracker(event.pointerId);
}
protected onPointerRemove(event: AdaptedEvent): void {
super.onPointerRemove(event);
this.tracker.removeFromTracker(event.pointerId);
}
}

View File

@@ -0,0 +1,166 @@
import { Platform } from 'react-native';
import { State } from '../../State';
import { DEFAULT_TOUCH_SLOP } from '../constants';
import { AdaptedEvent, Config } from '../interfaces';
import GestureHandler from './GestureHandler';
export default class NativeViewGestureHandler extends GestureHandler {
private buttonRole!: boolean;
//TODO: Implement logic for activation on start
//@ts-ignore Logic yet to be implemented
private shouldActivateOnStart = false;
private disallowInterruption = false;
private startX = 0;
private startY = 0;
private minDistSq = DEFAULT_TOUCH_SLOP * DEFAULT_TOUCH_SLOP;
public init(ref: number, propsRef: React.RefObject<unknown>): void {
super.init(ref, propsRef);
this.setShouldCancelWhenOutside(true);
if (Platform.OS !== 'web') {
return;
}
const view = this.delegate.getView() as HTMLElement;
view.style['touchAction'] = 'auto';
//@ts-ignore Turns on defualt touch behavior on Safari
view.style['WebkitTouchCallout'] = 'auto';
this.buttonRole = view.getAttribute('role') === 'button';
}
public updateGestureConfig({ enabled = true, ...props }: Config): void {
super.updateGestureConfig({ enabled: enabled, ...props });
if (this.config.shouldActivateOnStart !== undefined) {
this.shouldActivateOnStart = this.config.shouldActivateOnStart;
}
if (this.config.disallowInterruption !== undefined) {
this.disallowInterruption = this.config.disallowInterruption;
}
}
protected resetConfig(): void {
super.resetConfig();
}
protected onPointerDown(event: AdaptedEvent): void {
this.tracker.addToTracker(event);
super.onPointerDown(event);
this.newPointerAction();
}
protected onPointerAdd(event: AdaptedEvent): void {
this.tracker.addToTracker(event);
super.onPointerAdd(event);
this.newPointerAction();
}
private newPointerAction(): void {
this.startX = this.tracker.getLastAvgX();
this.startY = this.tracker.getLastAvgY();
if (this.currentState !== State.UNDETERMINED) {
return;
}
this.begin();
if (this.buttonRole) {
this.activate();
}
}
protected onPointerMove(event: AdaptedEvent): void {
this.tracker.track(event);
const dx = this.startX - this.tracker.getLastAvgX();
const dy = this.startY - this.tracker.getLastAvgY();
const distSq = dx * dx + dy * dy;
if (distSq >= this.minDistSq) {
if (this.buttonRole && this.currentState === State.ACTIVE) {
this.cancel();
} else if (!this.buttonRole && this.currentState === State.BEGAN) {
this.activate();
}
}
}
protected onPointerLeave(): void {
if (
this.currentState === State.BEGAN ||
this.currentState === State.ACTIVE
) {
this.cancel();
}
}
protected onPointerUp(event: AdaptedEvent): void {
super.onPointerUp(event);
this.onUp(event);
}
protected onPointerRemove(event: AdaptedEvent): void {
super.onPointerRemove(event);
this.onUp(event);
}
private onUp(event: AdaptedEvent): void {
this.tracker.removeFromTracker(event.pointerId);
if (this.tracker.getTrackedPointersCount() === 0) {
if (this.currentState === State.ACTIVE) {
this.end();
} else {
this.fail();
}
}
}
public shouldRecognizeSimultaneously(handler: GestureHandler): boolean {
if (super.shouldRecognizeSimultaneously(handler)) {
return true;
}
if (
handler instanceof NativeViewGestureHandler &&
handler.getState() === State.ACTIVE &&
handler.disallowsInterruption()
) {
return false;
}
const canBeInterrupted = !this.disallowInterruption;
if (
this.currentState === State.ACTIVE &&
handler.getState() === State.ACTIVE &&
canBeInterrupted
) {
return false;
}
return (
this.currentState === State.ACTIVE &&
canBeInterrupted &&
handler.getTag() > 0
);
}
public shouldBeCancelledByOther(_handler: GestureHandler): boolean {
return !this.disallowInterruption;
}
public disallowsInterruption(): boolean {
return this.disallowInterruption;
}
public isButton(): boolean {
return this.buttonRole;
}
}

View File

@@ -0,0 +1,493 @@
import { State } from '../../State';
import { DEFAULT_TOUCH_SLOP } from '../constants';
import { AdaptedEvent, Config } from '../interfaces';
import GestureHandler from './GestureHandler';
const DEFAULT_MIN_POINTERS = 1;
const DEFAULT_MAX_POINTERS = 10;
const DEFAULT_MIN_DIST_SQ = DEFAULT_TOUCH_SLOP * DEFAULT_TOUCH_SLOP;
export default class PanGestureHandler extends GestureHandler {
private readonly customActivationProperties: string[] = [
'activeOffsetXStart',
'activeOffsetXEnd',
'failOffsetXStart',
'failOffsetXEnd',
'activeOffsetYStart',
'activeOffsetYEnd',
'failOffsetYStart',
'failOffsetYEnd',
'minVelocityX',
'minVelocityY',
'minVelocity',
];
public velocityX = 0;
public velocityY = 0;
private minDistSq = DEFAULT_MIN_DIST_SQ;
private activeOffsetXStart = -Number.MAX_SAFE_INTEGER;
private activeOffsetXEnd = Number.MIN_SAFE_INTEGER;
private failOffsetXStart = Number.MIN_SAFE_INTEGER;
private failOffsetXEnd = Number.MAX_SAFE_INTEGER;
private activeOffsetYStart = Number.MAX_SAFE_INTEGER;
private activeOffsetYEnd = Number.MIN_SAFE_INTEGER;
private failOffsetYStart = Number.MIN_SAFE_INTEGER;
private failOffsetYEnd = Number.MAX_SAFE_INTEGER;
private minVelocityX = Number.MAX_SAFE_INTEGER;
private minVelocityY = Number.MAX_SAFE_INTEGER;
private minVelocitySq = Number.MAX_SAFE_INTEGER;
private minPointers = DEFAULT_MIN_POINTERS;
private maxPointers = DEFAULT_MAX_POINTERS;
private startX = 0;
private startY = 0;
private offsetX = 0;
private offsetY = 0;
private lastX = 0;
private lastY = 0;
private activateAfterLongPress = 0;
private activationTimeout = 0;
public init(ref: number, propsRef: React.RefObject<unknown>): void {
super.init(ref, propsRef);
}
public updateGestureConfig({ enabled = true, ...props }: Config): void {
this.resetConfig();
super.updateGestureConfig({ enabled: enabled, ...props });
this.checkCustomActivationCriteria(this.customActivationProperties);
if (this.config.minDist !== undefined) {
this.minDistSq = this.config.minDist * this.config.minDist;
} else if (this.hasCustomActivationCriteria) {
this.minDistSq = Number.MAX_SAFE_INTEGER;
}
if (this.config.minPointers !== undefined) {
this.minPointers = this.config.minPointers;
}
if (this.config.maxPointers !== undefined) {
this.maxPointers = this.config.maxPointers;
}
if (this.config.minVelocity !== undefined) {
this.minVelocityX = this.config.minVelocity;
this.minVelocityY = this.config.minVelocity;
}
if (this.config.minVelocityX !== undefined) {
this.minVelocityX = this.config.minVelocityX;
}
if (this.config.minVelocityY !== undefined) {
this.minVelocityY = this.config.minVelocityY;
}
if (this.config.activateAfterLongPress !== undefined) {
this.activateAfterLongPress = this.config.activateAfterLongPress;
}
if (this.config.activeOffsetXStart !== undefined) {
this.activeOffsetXStart = this.config.activeOffsetXStart;
if (this.config.activeOffsetXEnd === undefined) {
this.activeOffsetXEnd = Number.MAX_SAFE_INTEGER;
}
}
if (this.config.activeOffsetXEnd !== undefined) {
this.activeOffsetXEnd = this.config.activeOffsetXEnd;
if (this.config.activeOffsetXStart === undefined) {
this.activeOffsetXStart = Number.MIN_SAFE_INTEGER;
}
}
if (this.config.failOffsetXStart !== undefined) {
this.failOffsetXStart = this.config.failOffsetXStart;
if (this.config.failOffsetXEnd === undefined) {
this.failOffsetXEnd = Number.MAX_SAFE_INTEGER;
}
}
if (this.config.failOffsetXEnd !== undefined) {
this.failOffsetXEnd = this.config.failOffsetXEnd;
if (this.config.failOffsetXStart === undefined) {
this.failOffsetXStart = Number.MIN_SAFE_INTEGER;
}
}
if (this.config.activeOffsetYStart !== undefined) {
this.activeOffsetYStart = this.config.activeOffsetYStart;
if (this.config.activeOffsetYEnd === undefined) {
this.activeOffsetYEnd = Number.MAX_SAFE_INTEGER;
}
}
if (this.config.activeOffsetYEnd !== undefined) {
this.activeOffsetYEnd = this.config.activeOffsetYEnd;
if (this.config.activeOffsetYStart === undefined) {
this.activeOffsetYStart = Number.MIN_SAFE_INTEGER;
}
}
if (this.config.failOffsetYStart !== undefined) {
this.failOffsetYStart = this.config.failOffsetYStart;
if (this.config.failOffsetYEnd === undefined) {
this.failOffsetYEnd = Number.MAX_SAFE_INTEGER;
}
}
if (this.config.failOffsetYEnd !== undefined) {
this.failOffsetYEnd = this.config.failOffsetYEnd;
if (this.config.failOffsetYStart === undefined) {
this.failOffsetYStart = Number.MIN_SAFE_INTEGER;
}
}
}
protected resetConfig(): void {
super.resetConfig();
this.activeOffsetXStart = -Number.MAX_SAFE_INTEGER;
this.activeOffsetXEnd = Number.MIN_SAFE_INTEGER;
this.failOffsetXStart = Number.MIN_SAFE_INTEGER;
this.failOffsetXEnd = Number.MAX_SAFE_INTEGER;
this.activeOffsetYStart = Number.MAX_SAFE_INTEGER;
this.activeOffsetYEnd = Number.MIN_SAFE_INTEGER;
this.failOffsetYStart = Number.MIN_SAFE_INTEGER;
this.failOffsetYEnd = Number.MAX_SAFE_INTEGER;
this.minVelocityX = Number.MAX_SAFE_INTEGER;
this.minVelocityY = Number.MAX_SAFE_INTEGER;
this.minVelocitySq = Number.MAX_SAFE_INTEGER;
this.minDistSq = DEFAULT_MIN_DIST_SQ;
this.minPointers = DEFAULT_MIN_POINTERS;
this.maxPointers = DEFAULT_MAX_POINTERS;
this.activateAfterLongPress = 0;
}
protected transformNativeEvent() {
const translationX: number = this.getTranslationX();
const translationY: number = this.getTranslationY();
return {
...super.transformNativeEvent(),
translationX: isNaN(translationX) ? 0 : translationX,
translationY: isNaN(translationY) ? 0 : translationY,
velocityX: this.velocityX,
velocityY: this.velocityY,
};
}
private getTranslationX(): number {
return this.lastX - this.startX + this.offsetX;
}
private getTranslationY(): number {
return this.lastY - this.startY + this.offsetY;
}
private clearActivationTimeout(): void {
clearTimeout(this.activationTimeout);
}
//EventsHandling
protected onPointerDown(event: AdaptedEvent): void {
if (!this.isButtonInConfig(event.button)) {
return;
}
this.tracker.addToTracker(event);
super.onPointerDown(event);
this.lastX = this.tracker.getLastAvgX();
this.lastY = this.tracker.getLastAvgY();
this.startX = this.lastX;
this.startY = this.lastY;
this.tryBegin(event);
this.checkBegan();
}
protected onPointerAdd(event: AdaptedEvent): void {
this.tracker.addToTracker(event);
super.onPointerAdd(event);
this.tryBegin(event);
this.offsetX += this.lastX - this.startX;
this.offsetY += this.lastY - this.startY;
this.lastX = this.tracker.getLastAvgX();
this.lastY = this.tracker.getLastAvgY();
this.startX = this.lastX;
this.startY = this.lastY;
if (this.tracker.getTrackedPointersCount() > this.maxPointers) {
if (this.currentState === State.ACTIVE) {
this.cancel();
} else {
this.fail();
}
} else {
this.checkBegan();
}
}
protected onPointerUp(event: AdaptedEvent): void {
super.onPointerUp(event);
if (this.currentState === State.ACTIVE) {
this.lastX = this.tracker.getLastAvgX();
this.lastY = this.tracker.getLastAvgY();
}
this.tracker.removeFromTracker(event.pointerId);
if (this.currentState === State.ACTIVE) {
this.end();
} else {
this.resetProgress();
this.fail();
}
}
protected onPointerRemove(event: AdaptedEvent): void {
super.onPointerRemove(event);
this.tracker.removeFromTracker(event.pointerId);
this.offsetX += this.lastX - this.startX;
this.offsetY += this.lastY - this.startY;
this.lastX = this.tracker.getLastAvgX();
this.lastY = this.tracker.getLastAvgY();
this.startX = this.lastX;
this.startY = this.lastY;
if (
!(
this.currentState === State.ACTIVE &&
this.tracker.getTrackedPointersCount() < this.minPointers
)
) {
this.checkBegan();
}
}
protected onPointerMove(event: AdaptedEvent): void {
this.tracker.track(event);
this.lastX = this.tracker.getLastAvgX();
this.lastY = this.tracker.getLastAvgY();
this.velocityX = this.tracker.getVelocityX(event.pointerId);
this.velocityY = this.tracker.getVelocityY(event.pointerId);
this.checkBegan();
super.onPointerMove(event);
}
protected onPointerOutOfBounds(event: AdaptedEvent): void {
if (this.getShouldCancelWhenOutside()) {
return;
}
this.tracker.track(event);
this.lastX = this.tracker.getLastAvgX();
this.lastY = this.tracker.getLastAvgY();
this.velocityX = this.tracker.getVelocityX(event.pointerId);
this.velocityY = this.tracker.getVelocityY(event.pointerId);
this.checkBegan();
if (this.currentState === State.ACTIVE) {
super.onPointerOutOfBounds(event);
}
}
private shouldActivate(): boolean {
const dx: number = this.getTranslationX();
if (
this.activeOffsetXStart !== Number.MAX_SAFE_INTEGER &&
dx < this.activeOffsetXStart
) {
return true;
}
if (
this.activeOffsetXEnd !== Number.MIN_SAFE_INTEGER &&
dx > this.activeOffsetXEnd
) {
return true;
}
const dy: number = this.getTranslationY();
if (
this.activeOffsetYStart !== Number.MAX_SAFE_INTEGER &&
dy < this.activeOffsetYStart
) {
return true;
}
if (
this.activeOffsetYEnd !== Number.MIN_SAFE_INTEGER &&
dy > this.activeOffsetYEnd
) {
return true;
}
const distanceSq: number = dx * dx + dy * dy;
if (
this.minDistSq !== Number.MAX_SAFE_INTEGER &&
distanceSq >= this.minDistSq
) {
return true;
}
const vx: number = this.velocityX;
if (
this.minVelocityX !== Number.MAX_SAFE_INTEGER &&
((this.minVelocityX < 0 && vx <= this.minVelocityX) ||
(this.minVelocityX >= 0 && this.minVelocityX <= vx))
) {
return true;
}
const vy: number = this.velocityY;
if (
this.minVelocityY !== Number.MAX_SAFE_INTEGER &&
((this.minVelocityY < 0 && vy <= this.minVelocityY) ||
(this.minVelocityY >= 0 && this.minVelocityY <= vy))
) {
return true;
}
const velocitySq: number = vx * vx + vy * vy;
return (
this.minVelocitySq !== Number.MAX_SAFE_INTEGER &&
velocitySq >= this.minVelocitySq
);
}
private shouldFail(): boolean {
const dx: number = this.getTranslationX();
const dy: number = this.getTranslationY();
const distanceSq = dx * dx + dy * dy;
if (this.activateAfterLongPress > 0 && distanceSq > DEFAULT_MIN_DIST_SQ) {
this.clearActivationTimeout();
return true;
}
if (
this.failOffsetXStart !== Number.MIN_SAFE_INTEGER &&
dx < this.failOffsetXStart
) {
return true;
}
if (
this.failOffsetXEnd !== Number.MAX_SAFE_INTEGER &&
dx > this.failOffsetXEnd
) {
return true;
}
if (
this.failOffsetYStart !== Number.MIN_SAFE_INTEGER &&
dy < this.failOffsetYStart
) {
return true;
}
return (
this.failOffsetYEnd !== Number.MAX_SAFE_INTEGER &&
dy > this.failOffsetYEnd
);
}
private tryBegin(event: AdaptedEvent): void {
if (
this.currentState === State.UNDETERMINED &&
this.tracker.getTrackedPointersCount() >= this.minPointers
) {
this.resetProgress();
this.offsetX = 0;
this.offsetY = 0;
this.velocityX = 0;
this.velocityY = 0;
this.begin();
if (this.activateAfterLongPress > 0) {
this.activationTimeout = setTimeout(() => {
this.activate();
}, this.activateAfterLongPress);
}
} else {
this.velocityX = this.tracker.getVelocityX(event.pointerId);
this.velocityY = this.tracker.getVelocityY(event.pointerId);
}
}
private checkBegan(): void {
if (this.currentState === State.BEGAN) {
if (this.shouldFail()) {
this.fail();
} else if (this.shouldActivate()) {
this.activate();
}
}
}
public activate(force = false): void {
if (this.currentState !== State.ACTIVE) {
this.resetProgress();
}
super.activate(force);
}
protected onCancel(): void {
this.clearActivationTimeout();
}
protected onReset(): void {
this.clearActivationTimeout();
}
protected resetProgress(): void {
if (this.currentState === State.ACTIVE) {
return;
}
this.startX = this.lastX;
this.startY = this.lastY;
}
}

View File

@@ -0,0 +1,158 @@
import { State } from '../../State';
import { DEFAULT_TOUCH_SLOP } from '../constants';
import { AdaptedEvent, Config } from '../interfaces';
import GestureHandler from './GestureHandler';
import ScaleGestureDetector, {
ScaleGestureListener,
} from '../detectors/ScaleGestureDetector';
export default class PinchGestureHandler extends GestureHandler {
private scale = 1;
private velocity = 0;
private startingSpan = 0;
private spanSlop = DEFAULT_TOUCH_SLOP;
private scaleDetectorListener: ScaleGestureListener = {
onScaleBegin: (detector: ScaleGestureDetector): boolean => {
this.startingSpan = detector.getCurrentSpan();
return true;
},
onScale: (detector: ScaleGestureDetector): boolean => {
const prevScaleFactor: number = this.scale;
this.scale *= detector.getScaleFactor(
this.tracker.getTrackedPointersCount()
);
const delta = detector.getTimeDelta();
if (delta > 0) {
this.velocity = (this.scale - prevScaleFactor) / delta;
}
if (
Math.abs(this.startingSpan - detector.getCurrentSpan()) >=
this.spanSlop &&
this.currentState === State.BEGAN
) {
this.activate();
}
return true;
},
onScaleEnd: (
_detector: ScaleGestureDetector
// eslint-disable-next-line @typescript-eslint/no-empty-function
): void => {},
};
private scaleGestureDetector: ScaleGestureDetector = new ScaleGestureDetector(
this.scaleDetectorListener
);
public init(ref: number, propsRef: React.RefObject<unknown>) {
super.init(ref, propsRef);
this.setShouldCancelWhenOutside(false);
}
public updateGestureConfig({ enabled = true, ...props }: Config): void {
super.updateGestureConfig({ enabled: enabled, ...props });
}
protected transformNativeEvent() {
return {
focalX: this.scaleGestureDetector.getFocusX(),
focalY: this.scaleGestureDetector.getFocusY(),
velocity: this.velocity,
scale: this.scale,
};
}
protected onPointerDown(event: AdaptedEvent): void {
this.tracker.addToTracker(event);
super.onPointerDown(event);
}
protected onPointerAdd(event: AdaptedEvent): void {
this.tracker.addToTracker(event);
super.onPointerAdd(event);
this.tryBegin();
this.scaleGestureDetector.onTouchEvent(event, this.tracker);
}
protected onPointerUp(event: AdaptedEvent): void {
super.onPointerUp(event);
this.tracker.removeFromTracker(event.pointerId);
if (this.currentState !== State.ACTIVE) {
return;
}
this.scaleGestureDetector.onTouchEvent(event, this.tracker);
if (this.currentState === State.ACTIVE) {
this.end();
} else {
this.fail();
}
}
protected onPointerRemove(event: AdaptedEvent): void {
super.onPointerRemove(event);
this.scaleGestureDetector.onTouchEvent(event, this.tracker);
this.tracker.removeFromTracker(event.pointerId);
if (
this.currentState === State.ACTIVE &&
this.tracker.getTrackedPointersCount() < 2
) {
this.end();
}
}
protected onPointerMove(event: AdaptedEvent): void {
if (this.tracker.getTrackedPointersCount() < 2) {
return;
}
this.tracker.track(event);
this.scaleGestureDetector.onTouchEvent(event, this.tracker);
super.onPointerMove(event);
}
protected onPointerOutOfBounds(event: AdaptedEvent): void {
if (this.tracker.getTrackedPointersCount() < 2) {
return;
}
this.tracker.track(event);
this.scaleGestureDetector.onTouchEvent(event, this.tracker);
super.onPointerOutOfBounds(event);
}
private tryBegin(): void {
if (this.currentState !== State.UNDETERMINED) {
return;
}
this.resetProgress();
this.begin();
}
public activate(force?: boolean): void {
if (this.currentState !== State.ACTIVE) {
this.resetProgress();
}
super.activate(force);
}
protected onReset(): void {
this.resetProgress();
}
protected resetProgress(): void {
if (this.currentState === State.ACTIVE) {
return;
}
this.velocity = 0;
this.scale = 1;
}
}

View File

@@ -0,0 +1,172 @@
import { State } from '../../State';
import { AdaptedEvent, Config } from '../interfaces';
import GestureHandler from './GestureHandler';
import RotationGestureDetector, {
RotationGestureListener,
} from '../detectors/RotationGestureDetector';
const ROTATION_RECOGNITION_THRESHOLD = Math.PI / 36;
export default class RotationGestureHandler extends GestureHandler {
private rotation = 0;
private velocity = 0;
private cachedAnchorX = 0;
private cachedAnchorY = 0;
private rotationGestureListener: RotationGestureListener = {
onRotationBegin: (_detector: RotationGestureDetector): boolean => true,
onRotation: (detector: RotationGestureDetector): boolean => {
const previousRotation: number = this.rotation;
this.rotation += detector.getRotation();
const delta = detector.getTimeDelta();
if (delta > 0) {
this.velocity = (this.rotation - previousRotation) / delta;
}
if (
Math.abs(this.rotation) >= ROTATION_RECOGNITION_THRESHOLD &&
this.currentState === State.BEGAN
) {
this.activate();
}
return true;
},
onRotationEnd: (_detector: RotationGestureDetector): void => {
this.end();
},
};
private rotationGestureDetector: RotationGestureDetector =
new RotationGestureDetector(this.rotationGestureListener);
public init(ref: number, propsRef: React.RefObject<unknown>): void {
super.init(ref, propsRef);
this.setShouldCancelWhenOutside(false);
}
public updateGestureConfig({ enabled = true, ...props }: Config): void {
super.updateGestureConfig({ enabled: enabled, ...props });
}
protected transformNativeEvent() {
return {
rotation: this.rotation ? this.rotation : 0,
anchorX: this.getAnchorX(),
anchorY: this.getAnchorY(),
velocity: this.velocity ? this.velocity : 0,
};
}
public getAnchorX(): number {
const anchorX = this.rotationGestureDetector.getAnchorX();
return anchorX ? anchorX : this.cachedAnchorX;
}
public getAnchorY(): number {
const anchorY = this.rotationGestureDetector.getAnchorY();
return anchorY ? anchorY : this.cachedAnchorY;
}
protected onPointerDown(event: AdaptedEvent): void {
this.tracker.addToTracker(event);
super.onPointerDown(event);
}
protected onPointerAdd(event: AdaptedEvent): void {
this.tracker.addToTracker(event);
super.onPointerAdd(event);
this.tryBegin();
this.rotationGestureDetector.onTouchEvent(event, this.tracker);
}
protected onPointerMove(event: AdaptedEvent): void {
if (this.tracker.getTrackedPointersCount() < 2) {
return;
}
if (this.getAnchorX()) {
this.cachedAnchorX = this.getAnchorX();
}
if (this.getAnchorY()) {
this.cachedAnchorY = this.getAnchorY();
}
this.tracker.track(event);
this.rotationGestureDetector.onTouchEvent(event, this.tracker);
super.onPointerMove(event);
}
protected onPointerOutOfBounds(event: AdaptedEvent): void {
if (this.tracker.getTrackedPointersCount() < 2) {
return;
}
if (this.getAnchorX()) {
this.cachedAnchorX = this.getAnchorX();
}
if (this.getAnchorY()) {
this.cachedAnchorY = this.getAnchorY();
}
this.tracker.track(event);
this.rotationGestureDetector.onTouchEvent(event, this.tracker);
super.onPointerOutOfBounds(event);
}
protected onPointerUp(event: AdaptedEvent): void {
super.onPointerUp(event);
this.tracker.removeFromTracker(event.pointerId);
this.rotationGestureDetector.onTouchEvent(event, this.tracker);
if (this.currentState !== State.ACTIVE) {
return;
}
if (this.currentState === State.ACTIVE) {
this.end();
} else {
this.fail();
}
}
protected onPointerRemove(event: AdaptedEvent): void {
super.onPointerRemove(event);
this.rotationGestureDetector.onTouchEvent(event, this.tracker);
this.tracker.removeFromTracker(event.pointerId);
}
protected tryBegin(): void {
if (this.currentState !== State.UNDETERMINED) {
return;
}
this.begin();
}
public activate(_force?: boolean): void {
super.activate();
}
protected onReset(): void {
if (this.currentState === State.ACTIVE) {
return;
}
this.rotation = 0;
this.velocity = 0;
this.rotationGestureDetector.reset();
}
}

View File

@@ -0,0 +1,277 @@
import { State } from '../../State';
import { AdaptedEvent, Config, EventTypes } from '../interfaces';
import GestureHandler from './GestureHandler';
const DEFAULT_MAX_DURATION_MS = 500;
const DEFAULT_MAX_DELAY_MS = 500;
const DEFAULT_NUMBER_OF_TAPS = 1;
const DEFAULT_MIN_NUMBER_OF_POINTERS = 1;
export default class TapGestureHandler extends GestureHandler {
private maxDeltaX = Number.MIN_SAFE_INTEGER;
private maxDeltaY = Number.MIN_SAFE_INTEGER;
private maxDistSq = Number.MIN_SAFE_INTEGER;
private maxDurationMs = DEFAULT_MAX_DURATION_MS;
private maxDelayMs = DEFAULT_MAX_DELAY_MS;
private numberOfTaps = DEFAULT_NUMBER_OF_TAPS;
private minNumberOfPointers = DEFAULT_MIN_NUMBER_OF_POINTERS;
private currentMaxNumberOfPointers = 1;
private startX = 0;
private startY = 0;
private offsetX = 0;
private offsetY = 0;
private lastX = 0;
private lastY = 0;
private waitTimeout: number | undefined;
private delayTimeout: number | undefined;
private tapsSoFar = 0;
public init(ref: number, propsRef: React.RefObject<unknown>): void {
super.init(ref, propsRef);
}
public updateGestureConfig({ enabled = true, ...props }: Config): void {
super.updateGestureConfig({ enabled: enabled, ...props });
if (this.config.numberOfTaps !== undefined) {
this.numberOfTaps = this.config.numberOfTaps;
}
if (this.config.maxDurationMs !== undefined) {
this.maxDurationMs = this.config.maxDurationMs;
}
if (this.config.maxDelayMs !== undefined) {
this.maxDelayMs = this.config.maxDelayMs;
}
if (this.config.maxDeltaX !== undefined) {
this.maxDeltaX = this.config.maxDeltaX;
}
if (this.config.maxDeltaY !== undefined) {
this.maxDeltaY = this.config.maxDeltaY;
}
if (this.config.maxDist !== undefined) {
this.maxDistSq = this.config.maxDist * this.config.maxDist;
}
if (this.config.minPointers !== undefined) {
this.minNumberOfPointers = this.config.minPointers;
}
}
protected resetConfig(): void {
super.resetConfig();
this.maxDeltaX = Number.MIN_SAFE_INTEGER;
this.maxDeltaY = Number.MIN_SAFE_INTEGER;
this.maxDistSq = Number.MIN_SAFE_INTEGER;
this.maxDurationMs = DEFAULT_MAX_DURATION_MS;
this.maxDelayMs = DEFAULT_MAX_DELAY_MS;
this.numberOfTaps = DEFAULT_NUMBER_OF_TAPS;
this.minNumberOfPointers = DEFAULT_MIN_NUMBER_OF_POINTERS;
}
private clearTimeouts(): void {
clearTimeout(this.waitTimeout);
clearTimeout(this.delayTimeout);
}
private startTap(): void {
this.clearTimeouts();
this.waitTimeout = setTimeout(() => this.fail(), this.maxDurationMs);
}
private endTap(): void {
this.clearTimeouts();
if (
++this.tapsSoFar === this.numberOfTaps &&
this.currentMaxNumberOfPointers >= this.minNumberOfPointers
) {
this.activate();
} else {
this.delayTimeout = setTimeout(() => this.fail(), this.maxDelayMs);
}
}
//Handling Events
protected onPointerDown(event: AdaptedEvent): void {
if (!this.isButtonInConfig(event.button)) {
return;
}
this.tracker.addToTracker(event);
super.onPointerDown(event);
this.trySettingPosition(event);
this.startX = event.x;
this.startY = event.y;
this.lastX = event.x;
this.lastY = event.y;
this.updateState(event);
}
protected onPointerAdd(event: AdaptedEvent): void {
super.onPointerAdd(event);
this.tracker.addToTracker(event);
this.trySettingPosition(event);
this.offsetX += this.lastX - this.startX;
this.offsetY += this.lastY - this.startY;
this.lastX = this.tracker.getLastAvgX();
this.lastY = this.tracker.getLastAvgY();
this.startX = this.tracker.getLastAvgX();
this.startY = this.tracker.getLastAvgY();
this.updateState(event);
}
protected onPointerUp(event: AdaptedEvent): void {
super.onPointerUp(event);
this.lastX = this.tracker.getLastAvgX();
this.lastY = this.tracker.getLastAvgY();
this.tracker.removeFromTracker(event.pointerId);
this.updateState(event);
}
protected onPointerRemove(event: AdaptedEvent): void {
super.onPointerRemove(event);
this.tracker.removeFromTracker(event.pointerId);
this.offsetX += this.lastX - this.startX;
this.offsetY += this.lastY = this.startY;
this.lastX = this.tracker.getLastAvgX();
this.lastY = this.tracker.getLastAvgY();
this.startX = this.lastX;
this.startY = this.lastY;
this.updateState(event);
}
protected onPointerMove(event: AdaptedEvent): void {
this.trySettingPosition(event);
this.tracker.track(event);
this.lastX = this.tracker.getLastAvgX();
this.lastY = this.tracker.getLastAvgY();
this.updateState(event);
super.onPointerMove(event);
}
protected onPointerOutOfBounds(event: AdaptedEvent): void {
this.trySettingPosition(event);
this.tracker.track(event);
this.lastX = this.tracker.getLastAvgX();
this.lastY = this.tracker.getLastAvgY();
this.updateState(event);
super.onPointerOutOfBounds(event);
}
private updateState(event: AdaptedEvent): void {
if (
this.currentMaxNumberOfPointers < this.tracker.getTrackedPointersCount()
) {
this.currentMaxNumberOfPointers = this.tracker.getTrackedPointersCount();
}
if (this.shouldFail()) {
this.fail();
return;
}
switch (this.currentState) {
case State.UNDETERMINED:
if (event.eventType === EventTypes.DOWN) {
this.begin();
}
this.startTap();
break;
case State.BEGAN:
if (event.eventType === EventTypes.UP) {
this.endTap();
}
if (event.eventType === EventTypes.DOWN) {
this.startTap();
}
break;
default:
break;
}
}
private trySettingPosition(event: AdaptedEvent): void {
if (this.currentState !== State.UNDETERMINED) {
return;
}
this.offsetX = 0;
this.offsetY = 0;
this.startX = event.x;
this.startY = event.y;
}
private shouldFail(): boolean {
const dx = this.lastX - this.startX + this.offsetX;
if (
this.maxDeltaX !== Number.MIN_SAFE_INTEGER &&
Math.abs(dx) > this.maxDeltaX
) {
return true;
}
const dy = this.lastY - this.startY + this.offsetY;
if (
this.maxDeltaY !== Number.MIN_SAFE_INTEGER &&
Math.abs(dy) > this.maxDeltaY
) {
return true;
}
const distSq = dy * dy + dx * dx;
return (
this.maxDistSq !== Number.MIN_SAFE_INTEGER && distSq > this.maxDistSq
);
}
public activate(): void {
super.activate();
this.end();
}
protected onCancel(): void {
this.resetProgress();
this.clearTimeouts();
}
protected resetProgress(): void {
this.clearTimeouts();
this.tapsSoFar = 0;
this.currentMaxNumberOfPointers = 0;
}
}

View File

@@ -0,0 +1,166 @@
import {
UserSelect,
ActiveCursor,
MouseButton,
TouchAction,
} from '../handlers/gestureHandlerCommon';
import { Directions } from '../Directions';
import { State } from '../State';
import { PointerType } from '../PointerType';
export interface HitSlop {
left?: number;
right?: number;
top?: number;
bottom?: number;
horizontal?: number;
vertical?: number;
width?: number;
height?: number;
}
export interface Handler {
handlerTag: number;
}
type ConfigArgs =
| number
| boolean
| HitSlop
| UserSelect
| TouchAction
| ActiveCursor
| Directions
| Handler[]
| null
| undefined;
export interface Config extends Record<string, ConfigArgs> {
enabled?: boolean;
simultaneousHandlers?: Handler[] | null;
waitFor?: Handler[] | null;
blocksHandlers?: Handler[] | null;
hitSlop?: HitSlop;
shouldCancelWhenOutside?: boolean;
userSelect?: UserSelect;
activeCursor?: ActiveCursor;
mouseButton?: MouseButton;
enableContextMenu?: boolean;
touchAction?: TouchAction;
manualActivation?: boolean;
activateAfterLongPress?: number;
failOffsetXStart?: number;
failOffsetYStart?: number;
failOffsetXEnd?: number;
failOffsetYEnd?: number;
activeOffsetXStart?: number;
activeOffsetXEnd?: number;
activeOffsetYStart?: number;
activeOffsetYEnd?: number;
minPointers?: number;
maxPointers?: number;
minDist?: number;
minDistSq?: number;
minVelocity?: number;
minVelocityX?: number;
minVelocityY?: number;
minVelocitySq?: number;
maxDist?: number;
maxDistSq?: number;
numberOfPointers?: number;
minDurationMs?: number;
numberOfTaps?: number;
maxDurationMs?: number;
maxDelayMs?: number;
maxDeltaX?: number;
maxDeltaY?: number;
shouldActivateOnStart?: boolean;
disallowInterruption?: boolean;
direction?: Directions;
}
type NativeEventArgs = number | State | boolean | undefined;
interface NativeEvent extends Record<string, NativeEventArgs> {
numberOfPointers: number;
state: State;
pointerInside: boolean | undefined;
handlerTag: number;
target: number;
oldState?: State;
pointerType: PointerType;
}
export interface Point {
x: number;
y: number;
}
export interface PointerData {
id: number;
x: number;
y: number;
absoluteX: number;
absoluteY: number;
}
type TouchNativeArgs = number | State | TouchEventType | PointerData[];
interface NativeTouchEvent extends Record<string, TouchNativeArgs> {
handlerTag: number;
state: State;
eventType: TouchEventType;
changedTouches: PointerData[];
allTouches: PointerData[];
numberOfTouches: number;
}
export interface ResultEvent extends Record<string, NativeEvent | number> {
nativeEvent: NativeEvent;
timeStamp: number;
}
export interface ResultTouchEvent
extends Record<string, NativeTouchEvent | number> {
nativeEvent: NativeTouchEvent;
timeStamp: number;
}
export interface PropsRef {
onGestureHandlerEvent: () => void;
onGestureHandlerStateChange: () => void;
}
export interface AdaptedEvent {
x: number;
y: number;
offsetX: number;
offsetY: number;
pointerId: number;
eventType: EventTypes;
pointerType: PointerType;
time: number;
button?: MouseButton;
allTouches?: TouchList;
changedTouches?: TouchList;
touchEventType?: TouchEventType;
}
export enum EventTypes {
DOWN,
ADDITIONAL_POINTER_DOWN,
UP,
ADDITIONAL_POINTER_UP,
MOVE,
ENTER,
LEAVE,
CANCEL,
}
export enum TouchEventType {
UNDETERMINED,
DOWN,
MOVE,
UP,
CANCELLED,
}

View File

@@ -0,0 +1,42 @@
export default class CircularBuffer<T> {
private bufferSize: number;
private buffer: T[];
private index: number;
private actualSize: number;
constructor(size: number) {
this.bufferSize = size;
this.buffer = new Array<T>(size);
this.index = 0;
this.actualSize = 0;
}
public get size(): number {
return this.actualSize;
}
public push(element: T): void {
this.buffer[this.index] = element;
this.index = (this.index + 1) % this.bufferSize;
this.actualSize = Math.min(this.actualSize + 1, this.bufferSize);
}
public get(at: number): T {
if (this.actualSize === this.bufferSize) {
let index = (this.index + at) % this.bufferSize;
if (index < 0) {
index += this.bufferSize;
}
return this.buffer[index];
} else {
return this.buffer[at];
}
}
public clear(): void {
this.buffer = new Array<T>(this.bufferSize);
this.index = 0;
this.actualSize = 0;
}
}

View File

@@ -0,0 +1,106 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import { AdaptedEvent, EventTypes, TouchEventType } from '../interfaces';
type PointerEventCallback = (event: AdaptedEvent) => void;
export default abstract class EventManager<T> {
protected readonly view: T;
protected pointersInBounds: number[] = [];
protected activePointersCounter: number;
constructor(view: T) {
this.view = view;
this.activePointersCounter = 0;
}
public abstract registerListeners(): void;
public abstract unregisterListeners(): void;
protected abstract mapEvent(
event: Event,
eventType: EventTypes,
index?: number,
touchEventType?: TouchEventType
): AdaptedEvent;
protected onPointerDown(_event: AdaptedEvent): void {}
protected onPointerAdd(_event: AdaptedEvent): void {}
protected onPointerUp(_event: AdaptedEvent): void {}
protected onPointerRemove(_event: AdaptedEvent): void {}
protected onPointerMove(_event: AdaptedEvent): void {}
protected onPointerLeave(_event: AdaptedEvent): void {} // called only when pointer is pressed (or touching)
protected onPointerEnter(_event: AdaptedEvent): void {} // called only when pointer is pressed (or touching)
protected onPointerCancel(_event: AdaptedEvent): void {
// When pointer cancel is triggered and there are more pointers on the view, only one pointer is cancelled
// Because we want all pointers to be cancelled by that event, we are doing it manually by reseting handler and changing activePointersCounter to 0
// Events that correspond to removing the pointer (pointerup, touchend) have condition, that they don't perform any action when activePointersCounter
// is equal to 0. This prevents counter from going to negative values, when pointers are removed from view after one of them has been cancelled
}
protected onPointerOutOfBounds(_event: AdaptedEvent): void {}
protected onPointerMoveOver(_event: AdaptedEvent): void {}
protected onPointerMoveOut(_event: AdaptedEvent): void {}
public setOnPointerDown(callback: PointerEventCallback): void {
this.onPointerDown = callback;
}
public setOnPointerAdd(callback: PointerEventCallback): void {
this.onPointerAdd = callback;
}
public setOnPointerUp(callback: PointerEventCallback): void {
this.onPointerUp = callback;
}
public setOnPointerRemove(callback: PointerEventCallback): void {
this.onPointerRemove = callback;
}
public setOnPointerMove(callback: PointerEventCallback): void {
this.onPointerMove = callback;
}
public setOnPointerLeave(callback: PointerEventCallback): void {
this.onPointerLeave = callback;
}
public setOnPointerEnter(callback: PointerEventCallback): void {
this.onPointerEnter = callback;
}
public setOnPointerCancel(callback: PointerEventCallback): void {
this.onPointerCancel = callback;
}
public setOnPointerOutOfBounds(callback: PointerEventCallback): void {
this.onPointerOutOfBounds = callback;
}
public setOnPointerMoveOver(callback: PointerEventCallback): void {
this.onPointerMoveOver = callback;
}
public setOnPointerMoveOut(callback: PointerEventCallback): void {
this.onPointerMoveOut = callback;
}
protected markAsInBounds(pointerId: number): void {
if (this.pointersInBounds.indexOf(pointerId) >= 0) {
return;
}
this.pointersInBounds.push(pointerId);
}
protected markAsOutOfBounds(pointerId: number): void {
const index: number = this.pointersInBounds.indexOf(pointerId);
if (index < 0) {
return;
}
this.pointersInBounds.splice(index, 1);
}
public resetManager(): void {
// Reseting activePointersCounter is necessary to make gestures such as pinch work properly
// There are gestures that end when there is still one active pointer (like pinch/rotation)
// When these gestures end, they are reset, but they still receive events from pointer that is active
// This causes trouble, since only onPointerDown registers gesture in orchestrator, and while gestures receive
// Events from active pointer after they finished, next pointerdown event will be registered as additional pointer, not the first one
// This casues trouble like gestures getting stuck in END state, even though they should have gone to UNDETERMINED
this.activePointersCounter = 0;
this.pointersInBounds = [];
}
}

Some files were not shown because too many files have changed in this diff Show More