UNPKG

react-native-gesture-handler

Version:

Declarative API exposing native platform touch and gesture system to React Native

579 lines (455 loc) 17.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _hammerjs = _interopRequireDefault(require("@egjs/hammerjs")); var _reactNative = require("react-native"); var _State = require("../State"); var _constants = require("./constants"); var NodeManager = _interopRequireWildcard(require("./NodeManager")); var _ghQueueMicrotask = require("../ghQueueMicrotask"); function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function () { return cache; }; return cache; } function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; } 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; } let gestureInstances = 0; class GestureHandler { get id() { return `${this.name}${this.gestureInstance}`; } // a simple way to check if GestureHandler is NativeViewGestureHandler, since importing it // here to use instanceof would cause import cycle get isNative() { return false; } get isDiscrete() { return false; } get shouldEnableGestureOnSetup() { throw new Error('Must override GestureHandler.shouldEnableGestureOnSetup'); } constructor() { _defineProperty(this, "handlerTag", void 0); _defineProperty(this, "isGestureRunning", false); _defineProperty(this, "view", null); _defineProperty(this, "hasCustomActivationCriteria", void 0); _defineProperty(this, "hasGestureFailed", false); _defineProperty(this, "hammer", null); _defineProperty(this, "initialRotation", null); _defineProperty(this, "__initialX", void 0); _defineProperty(this, "__initialY", void 0); _defineProperty(this, "config", {}); _defineProperty(this, "previousState", _State.State.UNDETERMINED); _defineProperty(this, "pendingGestures", {}); _defineProperty(this, "oldState", _State.State.UNDETERMINED); _defineProperty(this, "lastSentState", null); _defineProperty(this, "gestureInstance", void 0); _defineProperty(this, "_stillWaiting", void 0); _defineProperty(this, "propsRef", void 0); _defineProperty(this, "ref", void 0); _defineProperty(this, "clearSelfAsPending", () => { if (Array.isArray(this.config.waitFor)) { for (const gesture of this.config.waitFor) { gesture.removePendingGesture(this.id); } } }); _defineProperty(this, "destroy", () => { this.clearSelfAsPending(); if (this.hammer) { this.hammer.stop(false); this.hammer.destroy(); } this.hammer = null; }); _defineProperty(this, "isPointInView", ({ x, y }) => { // @ts-ignore FIXME(TS) const rect = this.view.getBoundingClientRect(); const pointerInside = x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom; return pointerInside; }); _defineProperty(this, "sendEvent", nativeEvent => { const { onGestureHandlerEvent, onGestureHandlerStateChange } = this.propsRef.current; const event = this.transformEventData(nativeEvent); invokeNullableMethod(onGestureHandlerEvent, event); if (this.lastSentState !== event.nativeEvent.state) { this.lastSentState = event.nativeEvent.state; invokeNullableMethod(onGestureHandlerStateChange, event); } }); _defineProperty(this, "sync", () => { const gesture = this.hammer.get(this.name); if (!gesture) return; const enable = (recognizer, inputData) => { if (!this.config.enabled) { this.isGestureRunning = false; this.hasGestureFailed = false; return false; } // Prevent events before the system is ready. if (!inputData || !recognizer.options || typeof inputData.maxPointers === 'undefined') { return this.shouldEnableGestureOnSetup; } if (this.hasGestureFailed) { return false; } if (!this.isDiscrete) { if (this.isGestureRunning) { return true; } // The built-in hammer.js "waitFor" doesn't work across multiple views. // Only process if there are views to wait for. this._stillWaiting = this._getPendingGestures(); // This gesture should continue waiting. if (this._stillWaiting.length) { // Check to see if one of the gestures you're waiting for has started. // If it has then the gesture should fail. for (const gesture of this._stillWaiting) { // When the target gesture has started, this gesture must force fail. if (!gesture.isDiscrete && gesture.isGestureRunning) { this.hasGestureFailed = true; this.isGestureRunning = false; return false; } } // This gesture shouldn't start until the others have finished. return false; } } // Use default behaviour if (!this.hasCustomActivationCriteria) { return true; } const deltaRotation = this.initialRotation == null ? 0 : inputData.rotation - this.initialRotation; // @ts-ignore FIXME(TS) const { success, failed } = this.isGestureEnabledForEvent(this.getConfig(), recognizer, { ...inputData, deltaRotation }); if (failed) { this.simulateCancelEvent(inputData); this.hasGestureFailed = true; } return success; }; const params = this.getHammerConfig(); // @ts-ignore FIXME(TS) gesture.set({ ...params, enable }); }); this.gestureInstance = gestureInstances++; this.hasCustomActivationCriteria = false; } getConfig() { return this.config; } onWaitingEnded(_gesture) {} removePendingGesture(id) { delete this.pendingGestures[id]; } addPendingGesture(gesture) { this.pendingGestures[gesture.id] = gesture; } isGestureEnabledForEvent(_config, _recognizer, _event) { return { success: true }; } get NativeGestureClass() { throw new Error('Must override GestureHandler.NativeGestureClass'); } updateHasCustomActivationCriteria(_config) { return true; } updateGestureConfig({ enabled = true, ...props }) { this.clearSelfAsPending(); this.config = this.ensureConfig({ enabled, ...props }); this.hasCustomActivationCriteria = this.updateHasCustomActivationCriteria(this.config); if (Array.isArray(this.config.waitFor)) { for (const gesture of this.config.waitFor) { gesture.addPendingGesture(this); } } if (this.hammer) { this.sync(); } return this.config; } getState(type) { // @ts-ignore TODO(TS) check if this is needed if (type == 0) { return 0; } return _constants.EventMap[type]; } transformEventData(event) { const { eventType, maxPointers: numberOfPointers } = event; // const direction = DirectionMap[ev.direction]; const changedTouch = event.changedPointers[0]; const pointerInside = this.isPointInView({ x: changedTouch.clientX, y: changedTouch.clientY }); // TODO(TS) Remove cast after https://github.com/DefinitelyTyped/DefinitelyTyped/pull/50966 is merged. const state = this.getState(eventType); if (state !== this.previousState) { this.oldState = this.previousState; this.previousState = state; } return { nativeEvent: { numberOfPointers, state, pointerInside, ...this.transformNativeEvent(event), // onHandlerStateChange only handlerTag: this.handlerTag, target: this.ref, // send oldState only when the state was changed, or is different than ACTIVE // GestureDetector relies on the presence of `oldState` to differentiate between // update events and state change events oldState: state !== this.previousState || state != 4 ? this.oldState : undefined }, timeStamp: Date.now() }; } transformNativeEvent(_event) { return {}; } cancelPendingGestures(event) { for (const gesture of Object.values(this.pendingGestures)) { if (gesture && gesture.isGestureRunning) { gesture.hasGestureFailed = true; gesture.cancelEvent(event); } } } notifyPendingGestures() { for (const gesture of Object.values(this.pendingGestures)) { if (gesture) { gesture.onWaitingEnded(this); } } } // FIXME event is undefined in runtime when firstly invoked (see Draggable example), check other functions taking event as input onGestureEnded(event) { this.isGestureRunning = false; this.cancelPendingGestures(event); } forceInvalidate(event) { if (this.isGestureRunning) { this.hasGestureFailed = true; this.cancelEvent(event); } } cancelEvent(event) { this.notifyPendingGestures(); this.sendEvent({ ...event, eventType: _hammerjs.default.INPUT_CANCEL, isFinal: true }); this.onGestureEnded(event); } onRawEvent({ isFirst }) { if (isFirst) { this.hasGestureFailed = false; } } shouldUseTouchEvents(config) { var _config$simultaneousH, _config$simultaneousH2; return (_config$simultaneousH = (_config$simultaneousH2 = config.simultaneousHandlers) === null || _config$simultaneousH2 === void 0 ? void 0 : _config$simultaneousH2.some(handler => handler.isNative)) !== null && _config$simultaneousH !== void 0 ? _config$simultaneousH : false; } setView(ref, propsRef) { if (ref == null) { this.destroy(); this.view = null; return; } // @ts-ignore window doesn't exist on global type as we don't want to use Node types const SUPPORTS_TOUCH = ('ontouchstart' in window); this.propsRef = propsRef; this.ref = ref; this.view = (0, _reactNative.findNodeHandle)(ref); // When the browser starts handling the gesture (e.g. scrolling), it sends a pointercancel event and stops // sending additional pointer events. This is not the case with touch events, so if the gesture is simultaneous // with a NativeGestureHandler, we need to check if touch events are supported and use them if possible. this.hammer = SUPPORTS_TOUCH && this.shouldUseTouchEvents(this.config) ? new _hammerjs.default.Manager(this.view, { inputClass: _hammerjs.default.TouchInput }) : new _hammerjs.default.Manager(this.view); this.oldState = _State.State.UNDETERMINED; this.previousState = _State.State.UNDETERMINED; this.lastSentState = null; const { NativeGestureClass } = this; // @ts-ignore TODO(TS) const gesture = new NativeGestureClass(this.getHammerConfig()); this.hammer.add(gesture); this.hammer.on('hammer.input', ev => { if (!this.config.enabled) { this.hasGestureFailed = false; this.isGestureRunning = false; return; } this.onRawEvent(ev); // TODO: Bacon: Check against something other than null // The isFirst value is not called when the first rotation is calculated. if (this.initialRotation === null && ev.rotation !== 0) { this.initialRotation = ev.rotation; } if (ev.isFinal) { // in favor of a willFail otherwise the last frame of the gesture will be captured. setTimeout(() => { this.initialRotation = null; this.hasGestureFailed = false; }); } }); this.setupEvents(); this.sync(); } setupEvents() { // TODO(TS) Hammer types aren't exactly that what we get in runtime if (!this.isDiscrete) { this.hammer.on(`${this.name}start`, event => this.onStart(event)); this.hammer.on(`${this.name}end ${this.name}cancel`, event => { this.onGestureEnded(event); }); } this.hammer.on(this.name, ev => this.onGestureActivated(ev)); // TODO(TS) remove cast after https://github.com/DefinitelyTyped/DefinitelyTyped/pull/50438 is merged } onStart({ deltaX, deltaY, rotation }) { // Reset the state for the next gesture this.oldState = _State.State.UNDETERMINED; this.previousState = _State.State.UNDETERMINED; this.lastSentState = null; this.isGestureRunning = true; this.__initialX = deltaX; this.__initialY = deltaY; this.initialRotation = rotation; } onGestureActivated(ev) { this.sendEvent(ev); } onSuccess() {} _getPendingGestures() { if (Array.isArray(this.config.waitFor) && this.config.waitFor.length) { // Get the list of gestures that this gesture is still waiting for. // Use `=== false` in case a ref that isn't a gesture handler is used. const stillWaiting = this.config.waitFor.filter(({ hasGestureFailed }) => hasGestureFailed === false); return stillWaiting; } return []; } getHammerConfig() { const pointers = this.config.minPointers === this.config.maxPointers ? this.config.minPointers : 0; return { pointers }; } simulateCancelEvent(_inputData) {} // Validate the props ensureConfig(config) { const props = { ...config }; // TODO(TS) We use ! to assert that if property is present then value is not empty (null, undefined) if ('minDist' in config) { props.minDist = config.minDist; props.minDistSq = props.minDist * props.minDist; } if ('minVelocity' in config) { props.minVelocity = config.minVelocity; props.minVelocitySq = props.minVelocity * props.minVelocity; } if ('maxDist' in config) { props.maxDist = config.maxDist; props.maxDistSq = config.maxDist * config.maxDist; } if ('waitFor' in config) { props.waitFor = asArray(config.waitFor).map(({ handlerTag }) => NodeManager.getHandler(handlerTag)).filter(v => v); } else { props.waitFor = null; } if ('simultaneousHandlers' in config) { const shouldUseTouchEvents = this.shouldUseTouchEvents(this.config); props.simultaneousHandlers = asArray(config.simultaneousHandlers).map(handler => { if (typeof handler === 'number') { return NodeManager.getHandler(handler); } else { return NodeManager.getHandler(handler.handlerTag); } }).filter(v => v); if (shouldUseTouchEvents !== this.shouldUseTouchEvents(props)) { (0, _ghQueueMicrotask.ghQueueMicrotask)(() => { // if the undelying event API needs to be changed, we need to unmount and mount // the hammer instance again. this.destroy(); this.setView(this.ref, this.propsRef); }); } } else { props.simultaneousHandlers = null; } const configProps = ['minPointers', 'maxPointers', 'minDist', 'maxDist', 'maxDistSq', 'minVelocitySq', 'minDistSq', 'minVelocity', 'failOffsetXStart', 'failOffsetYStart', 'failOffsetXEnd', 'failOffsetYEnd', 'activeOffsetXStart', 'activeOffsetXEnd', 'activeOffsetYStart', 'activeOffsetYEnd']; configProps.forEach(prop => { if (typeof props[prop] === 'undefined') { props[prop] = Number.NaN; } }); return props; // TODO(TS) how to convince TS that props are filled? } } // TODO(TS) investigate this method // Used for sending data to a callback or AnimatedEvent function invokeNullableMethod(method, event) { if (method) { if (typeof method === 'function') { method(event); } else { // For use with reanimated's AnimatedEvent if ('__getHandler' in method && typeof method.__getHandler === 'function') { const handler = method.__getHandler(); invokeNullableMethod(handler, event); } else { if ('__nodeConfig' in method) { const { argMapping } = method.__nodeConfig; if (Array.isArray(argMapping)) { for (const [index, [key, value]] of argMapping.entries()) { if (key in event.nativeEvent) { // @ts-ignore fix method type const nativeValue = event.nativeEvent[key]; if (value && value.setValue) { // Reanimated API value.setValue(nativeValue); } else { // RN Animated API method.__nodeConfig.argMapping[index] = [key, nativeValue]; } } } } } } } } } function asArray(value) { // TODO(TS) use config.waitFor type return value == null ? [] : Array.isArray(value) ? value : [value]; } var _default = GestureHandler; exports.default = _default; //# sourceMappingURL=GestureHandler.js.map