react-native-gesture-handler
Version:
Experimental implementation of a new declarative API for gesture handling in react-native
398 lines (333 loc) • 12.3 kB
text/typescript
import { State } from '../../State';
import { PointerType } from '../interfaces';
import GestureHandler from '../handlers/GestureHandler';
import PointerTracker from './PointerTracker';
import { isPointerInBounds } from '../utils';
export default class GestureHandlerOrchestrator {
private static instance: GestureHandlerOrchestrator;
private gestureHandlers: GestureHandler[] = [];
private awaitingHandlers: GestureHandler[] = [];
private handlersToCancel: GestureHandler[] = [];
private handlingChangeSemaphore = 0;
private activationIndex = 0;
// Private beacuse of Singleton
// eslint-disable-next-line no-useless-constructor, @typescript-eslint/no-empty-function
private constructor() {}
private scheduleFinishedHandlersCleanup(): void {
if (this.handlingChangeSemaphore === 0) {
this.cleanupFinishedHandlers();
}
}
private cleanHandler(handler: GestureHandler): void {
handler.reset();
handler.setActive(false);
handler.setAwaiting(false);
handler.setActivationIndex(Number.MAX_VALUE);
}
public removeHandlerFromOrchestrator(handler: GestureHandler): void {
this.gestureHandlers.splice(this.gestureHandlers.indexOf(handler), 1);
this.awaitingHandlers.splice(this.awaitingHandlers.indexOf(handler), 1);
this.handlersToCancel.splice(this.handlersToCancel.indexOf(handler), 1);
}
private cleanupFinishedHandlers(): void {
for (let i = this.gestureHandlers.length - 1; i >= 0; --i) {
const handler = this.gestureHandlers[i];
if (!handler) {
continue;
}
if (this.isFinished(handler.getState()) && !handler.isAwaiting()) {
this.gestureHandlers.splice(i, 1);
this.cleanHandler(handler);
}
}
}
private hasOtherHandlerToWaitFor(handler: GestureHandler): boolean {
let hasToWait = false;
this.gestureHandlers.forEach((otherHandler) => {
if (
otherHandler &&
!this.isFinished(otherHandler.getState()) &&
this.shouldHandlerWaitForOther(handler, otherHandler)
) {
hasToWait = true;
return;
}
});
return hasToWait;
}
private tryActivate(handler: GestureHandler): void {
if (this.hasOtherHandlerToWaitFor(handler)) {
this.addAwaitingHandler(handler);
} else if (
handler.getState() !== State.CANCELLED &&
handler.getState() !== State.FAILED
) {
if (this.shouldActivate(handler)) {
this.makeActive(handler);
} else {
switch (handler.getState()) {
case State.ACTIVE:
handler.fail();
break;
case State.BEGAN:
handler.cancel();
}
}
}
}
private shouldActivate(handler: GestureHandler): boolean {
for (const otherHandler of this.gestureHandlers) {
if (this.shouldHandlerBeCancelledBy(handler, otherHandler)) {
return false;
}
}
return true;
}
private cleanupAwaitingHandlers(handler: GestureHandler): void {
for (let i = 0; i < this.awaitingHandlers.length; ++i) {
if (
!this.awaitingHandlers[i].isAwaiting() &&
this.shouldHandlerWaitForOther(this.awaitingHandlers[i], handler)
) {
this.cleanHandler(this.awaitingHandlers[i]);
this.awaitingHandlers.splice(i, 1);
}
}
}
public onHandlerStateChange(
handler: GestureHandler,
newState: State,
oldState: State,
sendIfDisabled?: boolean
): void {
if (!handler.isEnabled() && !sendIfDisabled) {
return;
}
this.handlingChangeSemaphore += 1;
if (this.isFinished(newState)) {
this.awaitingHandlers.forEach((otherHandler) => {
if (this.shouldHandlerWaitForOther(otherHandler, handler)) {
if (newState === State.END) {
otherHandler?.cancel();
if (otherHandler.getState() === State.END) {
// Handle edge case, where discrete gestures end immediately after activation thus
// their state is set to END and when the gesture they are waiting for activates they
// should be cancelled, however `cancel` was never sent as gestures were already in the END state.
// Send synthetic BEGAN -> CANCELLED to properly handle JS logic
otherHandler.sendEvent(State.CANCELLED, State.BEGAN);
}
otherHandler?.setAwaiting(false);
} else {
this.tryActivate(otherHandler);
}
}
});
}
if (newState === State.ACTIVE) {
this.tryActivate(handler);
} else if (oldState === State.ACTIVE || oldState === State.END) {
if (handler.isActive()) {
handler.sendEvent(newState, oldState);
} else if (
oldState === State.ACTIVE &&
(newState === State.CANCELLED || newState === State.FAILED)
) {
handler.sendEvent(newState, State.BEGAN);
}
} else if (
oldState !== State.UNDETERMINED ||
newState !== State.CANCELLED
) {
handler.sendEvent(newState, oldState);
}
this.handlingChangeSemaphore -= 1;
this.scheduleFinishedHandlersCleanup();
if (this.awaitingHandlers.indexOf(handler) < 0) {
this.cleanupAwaitingHandlers(handler);
}
}
private makeActive(handler: GestureHandler): void {
const currentState = handler.getState();
handler.setActive(true);
handler.setShouldResetProgress(true);
handler.setActivationIndex(this.activationIndex++);
this.gestureHandlers.forEach((otherHandler) => {
// Order of arguments is correct - we check whether current handler should cancel existing handlers
if (this.shouldHandlerBeCancelledBy(otherHandler, handler)) {
this.handlersToCancel.push(otherHandler);
}
});
for (let i = this.handlersToCancel.length - 1; i >= 0; --i) {
this.handlersToCancel[i]?.cancel();
}
this.awaitingHandlers.forEach((otherHandler) => {
if (this.shouldHandlerBeCancelledBy(otherHandler, handler)) {
otherHandler?.cancel();
otherHandler?.setAwaiting(true);
}
});
handler.sendEvent(State.ACTIVE, State.BEGAN);
if (currentState !== State.ACTIVE) {
handler.sendEvent(State.END, State.ACTIVE);
if (currentState !== State.END) {
handler.sendEvent(State.UNDETERMINED, State.END);
}
}
if (handler.isAwaiting()) {
handler.setAwaiting(false);
for (let i = 0; i < this.awaitingHandlers.length; ++i) {
if (this.awaitingHandlers[i] === handler) {
this.awaitingHandlers.splice(i, 1);
}
}
}
this.handlersToCancel = [];
}
private addAwaitingHandler(handler: GestureHandler): void {
let alreadyExists = false;
this.awaitingHandlers.forEach((otherHandler) => {
if (otherHandler === handler) {
alreadyExists = true;
return;
}
});
if (alreadyExists) {
return;
}
this.awaitingHandlers.push(handler);
handler.setAwaiting(true);
handler.setActivationIndex(this.activationIndex++);
}
public recordHandlerIfNotPresent(handler: GestureHandler): void {
let alreadyExists = false;
this.gestureHandlers.forEach((otherHandler) => {
if (otherHandler === handler) {
alreadyExists = true;
return;
}
});
if (alreadyExists) {
return;
}
this.gestureHandlers.push(handler);
handler.setActive(false);
handler.setAwaiting(false);
handler.setActivationIndex(Number.MAX_SAFE_INTEGER);
}
private shouldHandlerWaitForOther(
handler: GestureHandler,
otherHandler: GestureHandler
): boolean {
return (
handler !== otherHandler &&
(handler.shouldWaitForHandlerFailure(otherHandler) ||
otherHandler.shouldRequireToWaitForFailure(handler))
);
}
private canRunSimultaneously(
gh1: GestureHandler,
gh2: GestureHandler
): boolean {
return (
gh1 === gh2 ||
gh1.shouldRecognizeSimultaneously(gh2) ||
gh2.shouldRecognizeSimultaneously(gh1)
);
}
private shouldHandlerBeCancelledBy(
handler: GestureHandler,
otherHandler: GestureHandler
): boolean {
if (this.canRunSimultaneously(handler, otherHandler)) {
return false;
}
if (
handler !== otherHandler &&
(handler.isAwaiting() || handler.getState() === State.ACTIVE)
) {
// For now it always returns false
return handler.shouldBeCancelledByOther(otherHandler);
}
const handlerPointers: number[] = handler.getTrackedPointersID();
const otherPointers: number[] = otherHandler.getTrackedPointersID();
if (
!PointerTracker.shareCommonPointers(handlerPointers, otherPointers) &&
handler.getView() !== otherHandler.getView()
) {
return this.checkOverlap(handler, otherHandler);
}
return true;
}
private checkOverlap(
handler: GestureHandler,
otherHandler: GestureHandler
): boolean {
// If handlers don't have common pointers, default return value is false.
// However, if at least on pointer overlaps with both handlers, we return true
// This solves issue in overlapping parents example
// TODO: Find better way to handle that issue, for example by activation order and handler cancelling
const handlerPointers: number[] = handler.getTrackedPointersID();
const otherPointers: number[] = otherHandler.getTrackedPointersID();
let overlap = false;
handlerPointers.forEach((pointer: number) => {
const handlerX: number = handler.getTracker().getLastX(pointer);
const handlerY: number = handler.getTracker().getLastY(pointer);
if (
isPointerInBounds(handler.getView(), { x: handlerX, y: handlerY }) &&
isPointerInBounds(otherHandler.getView(), { x: handlerX, y: handlerY })
) {
overlap = true;
}
});
otherPointers.forEach((pointer: number) => {
const otherX: number = otherHandler.getTracker().getLastX(pointer);
const otherY: number = otherHandler.getTracker().getLastY(pointer);
if (
isPointerInBounds(handler.getView(), { x: otherX, y: otherY }) &&
isPointerInBounds(otherHandler.getView(), { x: otherX, y: otherY })
) {
overlap = true;
}
});
return overlap;
}
private isFinished(state: State): boolean {
return (
state === State.END || state === State.FAILED || state === State.CANCELLED
);
}
// This function is called when handler receives touchdown event
// If handler is using mouse or pen as a pointer and any handler receives touch event,
// mouse/pen event dissappears - it doesn't send onPointerCancel nor onPointerUp (and others)
// This became a problem because handler was left at active state without any signal to end or fail
// To handle this, when new touch event is received, we loop through active handlers and check which type of
// pointer they're using. If there are any handler with mouse/pen as a pointer, we cancel them
public cancelMouseAndPenGestures(currentHandler: GestureHandler): void {
this.gestureHandlers.forEach((handler: GestureHandler) => {
if (
handler.getPointerType() !== PointerType.MOUSE &&
handler.getPointerType() !== PointerType.PEN
) {
return;
}
if (handler !== currentHandler) {
handler.cancel();
} else {
// Handler that received touch event should have its pointer tracker reset
// This allows handler to smoothly change from mouse/pen to touch
// The drawback is, that when we try to use mouse/pen one more time, it doesn't send onPointerDown at the first time
// so it is required to click two times to get handler to work
//
// However, handler will receive manually created onPointerEnter that is triggered in EventManager in onPointerMove method.
// There may be possibility to use that fact to make handler respond properly to first mouse click
handler.getTracker().resetTracker();
}
});
}
public static getInstance(): GestureHandlerOrchestrator {
if (!GestureHandlerOrchestrator.instance) {
GestureHandlerOrchestrator.instance = new GestureHandlerOrchestrator();
}
return GestureHandlerOrchestrator.instance;
}
}