react-native-gesture-handler
Version:
Experimental implementation of a new declarative API for gesture handling in react-native
362 lines (296 loc) • 12.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _State = require("../../State");
var _interfaces = require("../interfaces");
var _PointerTracker = _interopRequireDefault(require("./PointerTracker"));
var _utils = require("../utils");
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }
class GestureHandlerOrchestrator {
// Private beacuse of Singleton
// eslint-disable-next-line no-useless-constructor, @typescript-eslint/no-empty-function
constructor() {
_defineProperty(this, "gestureHandlers", []);
_defineProperty(this, "awaitingHandlers", []);
_defineProperty(this, "handlersToCancel", []);
_defineProperty(this, "handlingChangeSemaphore", 0);
_defineProperty(this, "activationIndex", 0);
}
scheduleFinishedHandlersCleanup() {
if (this.handlingChangeSemaphore === 0) {
this.cleanupFinishedHandlers();
}
}
cleanHandler(handler) {
handler.reset();
handler.setActive(false);
handler.setAwaiting(false);
handler.setActivationIndex(Number.MAX_VALUE);
}
removeHandlerFromOrchestrator(handler) {
this.gestureHandlers.splice(this.gestureHandlers.indexOf(handler), 1);
this.awaitingHandlers.splice(this.awaitingHandlers.indexOf(handler), 1);
this.handlersToCancel.splice(this.handlersToCancel.indexOf(handler), 1);
}
cleanupFinishedHandlers() {
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);
}
}
}
hasOtherHandlerToWaitFor(handler) {
let hasToWait = false;
this.gestureHandlers.forEach(otherHandler => {
if (otherHandler && !this.isFinished(otherHandler.getState()) && this.shouldHandlerWaitForOther(handler, otherHandler)) {
hasToWait = true;
return;
}
});
return hasToWait;
}
tryActivate(handler) {
if (this.hasOtherHandlerToWaitFor(handler)) {
this.addAwaitingHandler(handler);
} else if (handler.getState() !== _State.State.CANCELLED && handler.getState() !== _State.State.FAILED) {
if (this.shouldActivate(handler)) {
this.makeActive(handler);
} else {
switch (handler.getState()) {
case _State.State.ACTIVE:
handler.fail();
break;
case _State.State.BEGAN:
handler.cancel();
}
}
}
}
shouldActivate(handler) {
for (const otherHandler of this.gestureHandlers) {
if (this.shouldHandlerBeCancelledBy(handler, otherHandler)) {
return false;
}
}
return true;
}
cleanupAwaitingHandlers(handler) {
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);
}
}
}
onHandlerStateChange(handler, newState, oldState, sendIfDisabled) {
if (!handler.isEnabled() && !sendIfDisabled) {
return;
}
this.handlingChangeSemaphore += 1;
if (this.isFinished(newState)) {
this.awaitingHandlers.forEach(otherHandler => {
if (this.shouldHandlerWaitForOther(otherHandler, handler)) {
if (newState === _State.State.END) {
otherHandler === null || otherHandler === void 0 ? void 0 : otherHandler.cancel();
if (otherHandler.getState() === _State.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.State.CANCELLED, _State.State.BEGAN);
}
otherHandler === null || otherHandler === void 0 ? void 0 : otherHandler.setAwaiting(false);
} else {
this.tryActivate(otherHandler);
}
}
});
}
if (newState === _State.State.ACTIVE) {
this.tryActivate(handler);
} else if (oldState === _State.State.ACTIVE || oldState === _State.State.END) {
if (handler.isActive()) {
handler.sendEvent(newState, oldState);
} else if (oldState === _State.State.ACTIVE && (newState === _State.State.CANCELLED || newState === _State.State.FAILED)) {
handler.sendEvent(newState, _State.State.BEGAN);
}
} else if (oldState !== _State.State.UNDETERMINED || newState !== _State.State.CANCELLED) {
handler.sendEvent(newState, oldState);
}
this.handlingChangeSemaphore -= 1;
this.scheduleFinishedHandlersCleanup();
if (this.awaitingHandlers.indexOf(handler) < 0) {
this.cleanupAwaitingHandlers(handler);
}
}
makeActive(handler) {
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) {
var _this$handlersToCance;
(_this$handlersToCance = this.handlersToCancel[i]) === null || _this$handlersToCance === void 0 ? void 0 : _this$handlersToCance.cancel();
}
this.awaitingHandlers.forEach(otherHandler => {
if (this.shouldHandlerBeCancelledBy(otherHandler, handler)) {
otherHandler === null || otherHandler === void 0 ? void 0 : otherHandler.cancel();
otherHandler === null || otherHandler === void 0 ? void 0 : otherHandler.setAwaiting(true);
}
});
handler.sendEvent(_State.State.ACTIVE, _State.State.BEGAN);
if (currentState !== _State.State.ACTIVE) {
handler.sendEvent(_State.State.END, _State.State.ACTIVE);
if (currentState !== _State.State.END) {
handler.sendEvent(_State.State.UNDETERMINED, _State.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 = [];
}
addAwaitingHandler(handler) {
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++);
}
recordHandlerIfNotPresent(handler) {
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);
}
shouldHandlerWaitForOther(handler, otherHandler) {
return handler !== otherHandler && (handler.shouldWaitForHandlerFailure(otherHandler) || otherHandler.shouldRequireToWaitForFailure(handler));
}
canRunSimultaneously(gh1, gh2) {
return gh1 === gh2 || gh1.shouldRecognizeSimultaneously(gh2) || gh2.shouldRecognizeSimultaneously(gh1);
}
shouldHandlerBeCancelledBy(handler, otherHandler) {
if (this.canRunSimultaneously(handler, otherHandler)) {
return false;
}
if (handler !== otherHandler && (handler.isAwaiting() || handler.getState() === _State.State.ACTIVE)) {
// For now it always returns false
return handler.shouldBeCancelledByOther(otherHandler);
}
const handlerPointers = handler.getTrackedPointersID();
const otherPointers = otherHandler.getTrackedPointersID();
if (!_PointerTracker.default.shareCommonPointers(handlerPointers, otherPointers) && handler.getView() !== otherHandler.getView()) {
return this.checkOverlap(handler, otherHandler);
}
return true;
}
checkOverlap(handler, otherHandler) {
// 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 = handler.getTrackedPointersID();
const otherPointers = otherHandler.getTrackedPointersID();
let overlap = false;
handlerPointers.forEach(pointer => {
const handlerX = handler.getTracker().getLastX(pointer);
const handlerY = handler.getTracker().getLastY(pointer);
if ((0, _utils.isPointerInBounds)(handler.getView(), {
x: handlerX,
y: handlerY
}) && (0, _utils.isPointerInBounds)(otherHandler.getView(), {
x: handlerX,
y: handlerY
})) {
overlap = true;
}
});
otherPointers.forEach(pointer => {
const otherX = otherHandler.getTracker().getLastX(pointer);
const otherY = otherHandler.getTracker().getLastY(pointer);
if ((0, _utils.isPointerInBounds)(handler.getView(), {
x: otherX,
y: otherY
}) && (0, _utils.isPointerInBounds)(otherHandler.getView(), {
x: otherX,
y: otherY
})) {
overlap = true;
}
});
return overlap;
}
isFinished(state) {
return state === _State.State.END || state === _State.State.FAILED || state === _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
cancelMouseAndPenGestures(currentHandler) {
this.gestureHandlers.forEach(handler => {
if (handler.getPointerType() !== _interfaces.PointerType.MOUSE && handler.getPointerType() !== _interfaces.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();
}
});
}
static getInstance() {
if (!GestureHandlerOrchestrator.instance) {
GestureHandlerOrchestrator.instance = new GestureHandlerOrchestrator();
}
return GestureHandlerOrchestrator.instance;
}
}
exports.default = GestureHandlerOrchestrator;
_defineProperty(GestureHandlerOrchestrator, "instance", void 0);
//# sourceMappingURL=GestureHandlerOrchestrator.js.map