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
JavaScript
;
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;
}