react-native-gesture-handler
Version:
Declarative API exposing native platform touch and gesture system to React Native
306 lines (254 loc) • 8.08 kB
text/typescript
import { Platform } from 'react-native';
import { type ActionType } from '../../ActionType';
import { State } from '../../State';
import { deepEqual } from '../../utils';
import type { NativeHandlerData } from '../../v3/hooks/gestures/native/NativeTypes';
import type { HandlerData } from '../../v3/types';
import { SingleGestureName } from '../../v3/types';
import {
DEFAULT_TOUCH_SLOP,
NATIVE_GESTURE_ROLE_ATTRIBUTE,
} from '../constants';
import type { AdaptedEvent, Config, PropsRef } from '../interfaces';
import { NativeGestureRole } from '../interfaces';
import type { GestureHandlerDelegate } from '../tools/GestureHandlerDelegate';
import {
dispatchGestureLifecycleEvent,
GestureLifecycleEvent,
} from '../tools/GestureLifecycleEvents';
import GestureHandler from './GestureHandler';
import type IGestureHandler from './IGestureHandler';
export default class NativeViewGestureHandler extends GestureHandler {
public override readonly isContinuous = true;
private role: NativeGestureRole | null = null;
// TODO: Implement logic for activation on start properly
private shouldActivateOnStart = false;
private disallowInterruption = false;
private yieldsToContinuousGestures = false;
private startX = 0;
private startY = 0;
private minDistSq = DEFAULT_TOUCH_SLOP * DEFAULT_TOUCH_SLOP;
private lastActiveHandlerData: HandlerData<NativeHandlerData> | null = null;
public constructor(
delegate: GestureHandlerDelegate<unknown, IGestureHandler>
) {
super(delegate);
this.name = SingleGestureName.Native;
}
public override init(
ref: number,
propsRef: React.RefObject<PropsRef>,
actionType: ActionType
): void {
super.init(ref, propsRef, actionType);
this.shouldCancelWhenOutside = true;
if (Platform.OS !== 'web') {
return;
}
const view = this.delegate.view as HTMLElement;
this.restoreViewStyles(view);
if (this.usesNativeOrVirtualDetector()) {
this.role =
(view.getAttribute(
NATIVE_GESTURE_ROLE_ATTRIBUTE
) as NativeGestureRole) ?? null;
} else {
if (view.getAttribute('role') === 'button') {
this.role = NativeGestureRole.Button;
} else if (view.querySelector(':scope > input[role="switch"]') !== null) {
this.role = NativeGestureRole.Switch;
}
}
}
public override updateGestureConfig(config: Config): void {
super.updateGestureConfig(config);
if (config.shouldActivateOnStart !== undefined) {
this.shouldActivateOnStart = config.shouldActivateOnStart;
}
if (config.disallowInterruption !== undefined) {
this.disallowInterruption = config.disallowInterruption;
}
if (config.yieldsToContinuousGestures !== undefined) {
this.yieldsToContinuousGestures = config.yieldsToContinuousGestures;
}
const view = this.delegate.view as HTMLElement;
this.restoreViewStyles(view);
}
private restoreViewStyles(view: HTMLElement) {
if (!view) {
return;
}
view.style['touchAction'] = 'auto';
// @ts-ignore Turns on default touch behavior on Safari
view.style['WebkitTouchCallout'] = 'auto';
}
protected override onPointerDown(event: AdaptedEvent): void {
this.tracker.addToTracker(event);
super.onPointerDown(event);
this.newPointerAction();
}
protected override onPointerAdd(event: AdaptedEvent): void {
this.tracker.addToTracker(event);
super.onPointerAdd(event);
this.newPointerAction();
}
private newPointerAction(): void {
const lastCoords = this.tracker.getAbsoluteCoordsAverage();
this.startX = lastCoords.x;
this.startY = lastCoords.y;
if (this.state !== State.UNDETERMINED) {
return;
}
this.begin();
dispatchGestureLifecycleEvent(
this.delegate.view as HTMLElement | null,
GestureLifecycleEvent.Began
);
const view = this.delegate.view as HTMLElement;
const isRNGHText = view.hasAttribute('rnghtext');
if (
(this.role === NativeGestureRole.Button && this.shouldActivateOnStart) ||
this.role === NativeGestureRole.Switch ||
isRNGHText
) {
this.activate();
}
}
protected override onPointerMove(event: AdaptedEvent): void {
this.tracker.track(event);
const lastCoords = this.tracker.getAbsoluteCoordsAverage();
const dx = this.startX - lastCoords.x;
const dy = this.startY - lastCoords.y;
const distSq = dx * dx + dy * dy;
if (
this.role === NativeGestureRole.Switch ||
this.role === NativeGestureRole.Button
) {
return;
}
if (distSq >= this.minDistSq && this.state === State.BEGAN) {
this.activate();
}
}
protected override onPointerLeave(): void {
if (this.state === State.BEGAN || this.state === State.ACTIVE) {
this.cancel();
}
}
protected override onPointerUp(event: AdaptedEvent): void {
super.onPointerUp(event);
this.onUp(event);
}
protected override onPointerRemove(event: AdaptedEvent): void {
super.onPointerRemove(event);
this.onUp(event);
}
private onUp(event: AdaptedEvent): void {
this.tracker.removeFromTracker(event.pointerId);
if (this.tracker.trackedPointersCount === 0) {
if (
this.role === NativeGestureRole.Button &&
this.state === State.BEGAN
) {
this.activate();
}
if (this.state === State.ACTIVE) {
this.end();
} else {
this.fail();
}
}
}
public override shouldRecognizeSimultaneously(
handler: IGestureHandler
): boolean {
if (super.shouldRecognizeSimultaneously(handler)) {
return true;
}
if (
handler instanceof NativeViewGestureHandler &&
handler.state === State.ACTIVE &&
handler.disallowsInterruption() &&
!handler.yieldsToContinuousGestures
) {
return false;
}
const canBeInterrupted =
!this.disallowInterruption ||
(this.yieldsToContinuousGestures && handler.isContinuous);
if (
this.state === State.ACTIVE &&
handler.state === State.ACTIVE &&
canBeInterrupted
) {
return false;
}
return (
this.state === State.ACTIVE && canBeInterrupted && handler.handlerTag > 0
);
}
public override detach(): void {
super.detach();
this.role = null;
}
public override shouldBeCancelledByOther(handler: IGestureHandler): boolean {
return (
!this.disallowInterruption ||
(this.yieldsToContinuousGestures && handler.isContinuous)
);
}
public override shouldAttachGestureToChildView(): boolean {
return true;
}
public disallowsInterruption(): boolean {
return this.disallowInterruption;
}
public isButton(): boolean {
return this.role === NativeGestureRole.Button;
}
public override shouldBeginWithRecordedHandlers(
recorded: IGestureHandler[]
): boolean {
if (!this.isButton()) {
return true;
}
const self = this as IGestureHandler;
return recorded.every(
(other) =>
other.shouldRecognizeSimultaneously(self) ||
self.shouldRecognizeSimultaneously(other) ||
other.delegate.view === this.delegate.view ||
other.name === SingleGestureName.Hover
);
}
protected override onCancel(): void {
super.onCancel();
dispatchGestureLifecycleEvent(
this.delegate.view as HTMLElement | null,
GestureLifecycleEvent.Canceled
);
}
protected override transformNativeEvent(): Record<string, unknown> {
return {
pointerInside: this.delegate.isPointerInBounds(
this.tracker.getAbsoluteCoordsAverage()
),
};
}
protected override shouldSuppressActiveUpdate(
handlerData: HandlerData<NativeHandlerData>
): boolean {
if (
this.lastActiveHandlerData &&
deepEqual(this.lastActiveHandlerData, handlerData)
) {
return true;
}
this.lastActiveHandlerData = handlerData;
return false;
}
public override reset(): void {
super.reset();
this.lastActiveHandlerData = null;
}
}