UNPKG

react-native-easy-guesture-responder

Version:

A powerful and easy to use function for React Native guesture responders. Supporting both iOS and Android. Free and made possible along with costly maintenance and updates by [Lue Hang](https://www.facebook.com/lue.hang) (the author).

346 lines (311 loc) 13.8 kB
"use strict"; import {InteractionManager} from "react-native"; import TouchHistoryMath from "./TouchHistoryMath"; import {pinchDistance} from "./TouchDistanceMath"; import TimerMixin from "./TimerMixin"; const currentCentroidXOfTouchesChangedAfter = TouchHistoryMath.currentCentroidXOfTouchesChangedAfter; const currentCentroidYOfTouchesChangedAfter = TouchHistoryMath.currentCentroidYOfTouchesChangedAfter; const previousCentroidXOfTouchesChangedAfter = TouchHistoryMath.previousCentroidXOfTouchesChangedAfter; const previousCentroidYOfTouchesChangedAfter = TouchHistoryMath.previousCentroidYOfTouchesChangedAfter; const currentCentroidX = TouchHistoryMath.currentCentroidX; const currentCentroidY = TouchHistoryMath.currentCentroidY; const TAP_UP_TIME_THRESHOLD = 400; const TAP_MOVE_THRESHOLD = 10; const MOVE_THRESHOLD = 2; let DEV = false; function initializeGestureState (gestureState) { gestureState.moveX = 0; gestureState.moveY = 0; gestureState.x0 = 0; gestureState.y0 = 0; gestureState.dx = 0; gestureState.dy = 0; gestureState.vx = 0; gestureState.vy = 0; gestureState.numberActiveTouches = 0; // All `gestureState` accounts for timeStamps up until: gestureState._accountsForMovesUpTo = 0; gestureState.previousMoveX = 0; gestureState.previousMoveY = 0; gestureState.pinch = undefined; gestureState.previousPinch = undefined; gestureState.singleTapUp = false; gestureState.doubleTapUp = false; gestureState._singleTabFailed = false; } function updateGestureStateOnMove (gestureState, touchHistory, e) { const movedAfter = gestureState._accountsForMovesUpTo; const prevX = previousCentroidXOfTouchesChangedAfter(touchHistory, movedAfter); const x = currentCentroidXOfTouchesChangedAfter(touchHistory, movedAfter); const prevY = previousCentroidYOfTouchesChangedAfter(touchHistory, movedAfter); const y = currentCentroidYOfTouchesChangedAfter(touchHistory, movedAfter); const dx = x - prevX; const dy = y - prevY; gestureState.numberActiveTouches = touchHistory.numberActiveTouches; gestureState.moveX = x; gestureState.moveY = y; // TODO: This must be filtered intelligently. // const dt = touchHistory.mostRecentTimeStamp - movedAfter; const dt = convertToMillisecIfNeeded(touchHistory.mostRecentTimeStamp - movedAfter); gestureState.vx = dx / dt; gestureState.vy = dy / dt; gestureState.dx += dx; gestureState.dy += dy; gestureState._accountsForMovesUpTo = touchHistory.mostRecentTimeStamp; gestureState.previousMoveX = prevX; gestureState.previousMoveY = prevY; gestureState.pinch = pinchDistance(touchHistory, movedAfter, true); gestureState.previousPinch = pinchDistance(touchHistory, movedAfter, false); } function clearInteractionHandle (interactionState) { if (interactionState.handle) { InteractionManager.clearInteractionHandle(interactionState.handle); interactionState.handle = null; } } /** * Android is using nanoseconds while iOS is using milliseconds. * @param interval * @returns {*} */ function convertToMillisecIfNeeded (interval) { if (interval > 1000000) { return interval / 1000000; } return interval; } function cancelSingleTapConfirm (gestureState) { if (typeof gestureState._singleTapConfirmId !== "undefined") { TimerMixin.clearTimeout(gestureState._singleTapConfirmId); gestureState._singleTapConfirmId = undefined; } } /** * And every callback are called with an additional argument "gestureState". * @param config * @returns {{}} */ /** * And every callback are called with an additional argument "gestureState". * @param config * @param debug true to enable debug logs * @returns {{}} */ export default function create (config) { if (config.debug) { DEV = true; } const interactionState = { handle: null }; const gestureState = { // Useful for debugging stateID: Math.random() }; initializeGestureState(gestureState); const handlers = { onStartShouldSetResponder: function (e) { // eslint-disable-next-line no-console DEV && console.log("onStartShouldSetResponder..."); cancelSingleTapConfirm(gestureState); return config.onStartShouldSetResponder ? config.onStartShouldSetResponder(e, gestureState) : false; }, onMoveShouldSetResponder: function (e) { // eslint-disable-next-line no-console DEV && console.log("onMoveShouldSetResponder..."); return config.onMoveShouldSetResponder && effectiveMove(config, gestureState) ? config.onMoveShouldSetResponder(e, gestureState) : false; }, onStartShouldSetResponderCapture: function (e) { // eslint-disable-next-line no-console DEV && console.log("onStartShouldSetResponderCapture..."); cancelSingleTapConfirm(gestureState); // TODO: Actually, we should reinitialize the state any time // touches.length increases from 0 active to > 0 active. if (e.nativeEvent.touches.length === 1) { initializeGestureState(gestureState); } gestureState.numberActiveTouches = e.touchHistory.numberActiveTouches; return config.onStartShouldSetResponderCapture ? config.onStartShouldSetResponderCapture(e, gestureState) : false; }, onMoveShouldSetResponderCapture: function (e) { // eslint-disable-next-line no-console DEV && console.log("onMoveShouldSetResponderCapture..."); const touchHistory = e.touchHistory; // Responder system incorrectly dispatches should* to current responder // Filter out any touch moves past the first one - we would have // already processed multi-touch geometry during the first event. if (gestureState._accountsForMovesUpTo === touchHistory.mostRecentTimeStamp) { return false; } updateGestureStateOnMove(gestureState, touchHistory, e); return config.onMoveShouldSetResponderCapture && effectiveMove(config, gestureState) ? config.onMoveShouldSetResponderCapture(e, gestureState) : false; }, onResponderGrant: function (e) { // eslint-disable-next-line no-console DEV && console.log("onResponderGrant..."); cancelSingleTapConfirm(gestureState); if (!interactionState.handle) { interactionState.handle = InteractionManager.createInteractionHandle(); } gestureState._grantTimestamp = e.touchHistory.mostRecentTimeStamp; gestureState.x0 = currentCentroidX(e.touchHistory); gestureState.y0 = currentCentroidY(e.touchHistory); gestureState.dx = 0; gestureState.dy = 0; if (config.onResponderGrant) { config.onResponderGrant(e, gestureState); } // TODO: t7467124 investigate if this can be removed return config.onShouldBlockNativeResponder === undefined ? true : config.onShouldBlockNativeResponder(); }, onResponderReject: function (e) { // eslint-disable-next-line no-console DEV && console.log("onResponderReject..."); clearInteractionHandle(interactionState); config.onResponderReject && config.onResponderReject(e, gestureState); }, onResponderRelease: function (e) { if (gestureState.singleTapUp) { if (gestureState._lastSingleTapUp) { if ( convertToMillisecIfNeeded( e.touchHistory.mostRecentTimeStamp - gestureState._lastReleaseTimestamp ) < TAP_UP_TIME_THRESHOLD ) { const snapshot = Object.assign({}, gestureState); // eslint-disable-next-line no-console DEV && console.log("onResponderDoubleTapConfirmed..."); config.onResponderDoubleTapConfirmed && config.onResponderDoubleTapConfirmed(e, snapshot); gestureState.doubleTapUp = true; } } gestureState._lastSingleTapUp = true; // schedule to confirm single tap if (!gestureState.doubleTapUp) { const snapshot = Object.assign({}, gestureState); const timeoutId = TimerMixin.setTimeout(() => { if (gestureState._singleTapConfirmId === timeoutId) { // eslint-disable-next-line no-console DEV && console.log("onResponderSingleTapConfirmed..."); config.onResponderSingleTapConfirmed && config.onResponderSingleTapConfirmed(e, snapshot); } }, TAP_UP_TIME_THRESHOLD); gestureState._singleTapConfirmId = timeoutId; } } gestureState._lastReleaseTimestamp = e.touchHistory.mostRecentTimeStamp; // eslint-disable-next-line no-console DEV && console.log("onResponderRelease..." + JSON.stringify(gestureState)); clearInteractionHandle(interactionState); config.onResponderRelease && config.onResponderRelease(e, gestureState); initializeGestureState(gestureState); }, onResponderStart: function (e) { // eslint-disable-next-line no-console DEV && console.log("onResponderStart..."); const touchHistory = e.touchHistory; gestureState.numberActiveTouches = touchHistory.numberActiveTouches; if (config.onResponderStart) { config.onResponderStart(e, gestureState); } }, onResponderMove: function (e) { const touchHistory = e.touchHistory; // Guard against the dispatch of two touch moves when there are two // simultaneously changed touches. if (gestureState._accountsForMovesUpTo === touchHistory.mostRecentTimeStamp) { return; } // Filter out any touch moves past the first one - we would have // already processed multi-touch geometry during the first event. updateGestureStateOnMove(gestureState, touchHistory, e); // eslint-disable-next-line no-console DEV && console.log("onResponderMove..." + JSON.stringify(gestureState)); if (config.onResponderMove && effectiveMove(config, gestureState)) { config.onResponderMove(e, gestureState); } }, onResponderEnd: function (e) { const touchHistory = e.touchHistory; gestureState.numberActiveTouches = touchHistory.numberActiveTouches; if (touchHistory.numberActiveTouches > 0 || convertToMillisecIfNeeded( touchHistory.mostRecentTimeStamp - gestureState._grantTimestamp ) > TAP_UP_TIME_THRESHOLD || Math.abs(gestureState.dx) >= TAP_MOVE_THRESHOLD || Math.abs(gestureState.dy) >= TAP_MOVE_THRESHOLD ) { gestureState._singleTabFailed = true; } if (!gestureState._singleTabFailed) { gestureState.singleTapUp = true; } // eslint-disable-next-line no-console DEV && console.log("onResponderEnd..." + JSON.stringify(gestureState)); clearInteractionHandle(interactionState); config.onResponderEnd && config.onResponderEnd(e, gestureState); }, onResponderTerminate: function (e) { // eslint-disable-next-line no-console DEV && console.log("onResponderTerminate..."); clearInteractionHandle(interactionState); config.onResponderTerminate && config.onResponderTerminate(e, gestureState); initializeGestureState(gestureState); }, onResponderTerminationRequest: function (e) { // eslint-disable-next-line no-console DEV && console.log("onResponderTerminationRequest..."); return config.onResponderTerminationRequest ? config.onResponderTerminationRequest(e.gestureState) : true; } }; return {...handlers}; } /** * On Android devices, the default gesture responder is too * sensitive that a single tap(no move intended) may trigger a move event. * We can use a moveThreshold config to avoid those unwanted move events. * @param config * @param gestureState * @returns {boolean} */ function effectiveMove (config, gestureState) { if (gestureState.numberActiveTouches > 1) { // on iOS simulator, a pinch gesture(move with alt pressed) // will not change gestureState.dx(always 0) return true; } let moveThreshold = MOVE_THRESHOLD; if (typeof config.moveThreshold === "number") { moveThreshold = config.minMoveDistance; } if ( Math.abs(gestureState.dx) >= moveThreshold || Math.abs(gestureState.dy) >= moveThreshold ) { return true; } return false; }