UNPKG

@tamagui/react-native-web-lite

Version:
238 lines (237 loc) 14.7 kB
import { InteractionManager } from "@tamagui/react-native-web-internals"; import TouchHistoryMath from "../TouchHistoryMath"; var currentCentroidXOfTouchesChangedAfter = TouchHistoryMath.currentCentroidXOfTouchesChangedAfter, currentCentroidYOfTouchesChangedAfter = TouchHistoryMath.currentCentroidYOfTouchesChangedAfter, previousCentroidXOfTouchesChangedAfter = TouchHistoryMath.previousCentroidXOfTouchesChangedAfter, previousCentroidYOfTouchesChangedAfter = TouchHistoryMath.previousCentroidYOfTouchesChangedAfter, currentCentroidX = TouchHistoryMath.currentCentroidX, currentCentroidY = TouchHistoryMath.currentCentroidY, PanResponder = { /** * * A graphical explanation of the touch data flow: * * +----------------------------+ +--------------------------------+ * | ResponderTouchHistoryStore | |TouchHistoryMath | * +----------------------------+ +----------+---------------------+ * |Global store of touchHistory| |Allocation-less math util | * |including activeness, start | |on touch history (centroids | * |position, prev/cur position.| |and multitouch movement etc) | * | | | | * +----^-----------------------+ +----^---------------------------+ * | | * | (records relevant history | * | of touches relevant for | * | implementing higher level | * | gestures) | * | | * +----+-----------------------+ +----|---------------------------+ * | ResponderEventPlugin | | | Your App/Component | * +----------------------------+ +----|---------------------------+ * |Negotiates which view gets | Low level | | High level | * |onResponderMove events. | events w/ | +-+-------+ events w/ | * |Also records history into | touchHistory| | Pan | multitouch + | * |ResponderTouchHistoryStore. +---------------->Responder+-----> accumulative| * +----------------------------+ attached to | | | distance and | * each event | +---------+ velocity. | * | | * | | * +--------------------------------+ * * * * Gesture that calculates cumulative movement over time in a way that just * "does the right thing" for multiple touches. The "right thing" is very * nuanced. When moving two touches in opposite directions, the cumulative * distance is zero in each dimension. When two touches move in parallel five * pixels in the same direction, the cumulative distance is five, not ten. If * two touches start, one moves five in a direction, then stops and the other * touch moves fives in the same direction, the cumulative distance is ten. * * This logic requires a kind of processing of time "clusters" of touch events * so that two touch moves that essentially occur in parallel but move every * other frame respectively, are considered part of the same movement. * * Explanation of some of the non-obvious fields: * * - moveX/moveY: If no move event has been observed, then `(moveX, moveY)` is * invalid. If a move event has been observed, `(moveX, moveY)` is the * centroid of the most recently moved "cluster" of active touches. * (Currently all move have the same timeStamp, but later we should add some * threshold for what is considered to be "moving"). If a palm is * accidentally counted as a touch, but a finger is moving greatly, the palm * will move slightly, but we only want to count the single moving touch. * - x0/y0: Centroid location (non-cumulative) at the time of becoming * responder. * - dx/dy: Cumulative touch distance - not the same thing as sum of each touch * distance. Accounts for touch moves that are clustered together in time, * moving the same direction. Only valid when currently responder (otherwise, * it only represents the drag distance below the threshold). * - vx/vy: Velocity. */ _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, gestureState._accountsForMovesUpTo = 0; }, /** * This is nuanced and is necessary. It is incorrect to continuously take all * active *and* recently moved touches, find the centroid, and track how that * result changes over time. Instead, we must take all recently moved * touches, and calculate how the centroid has changed just for those * recently moved touches, and append that change to an accumulator. This is * to (at least) handle the case where the user is moving three fingers, and * then one of the fingers stops but the other two continue. * * This is very different than taking all of the recently moved touches and * storing their centroid as `dx/dy`. For correctness, we must *accumulate * changes* in the centroid of recently moved touches. * * There is also some nuance with how we handle multiple moved touches in a * single event. With the way `ReactNativeEventEmitter` dispatches touches as * individual events, multiple touches generate two 'move' events, each of * them triggering `onResponderMove`. But with the way `PanResponder` works, * all of the gesture inference is performed on the first dispatch, since it * looks at all of the touches (even the ones for which there hasn't been a * native dispatch yet). Therefore, `PanResponder` does not call * `onResponderMove` passed the first dispatch. This diverges from the * typical responder callback pattern (without using `PanResponder`), but * avoids more dispatches than necessary. */ _updateGestureStateOnMove(gestureState, touchHistory) { gestureState.numberActiveTouches = touchHistory.numberActiveTouches, gestureState.moveX = currentCentroidXOfTouchesChangedAfter(touchHistory, gestureState._accountsForMovesUpTo), gestureState.moveY = currentCentroidYOfTouchesChangedAfter(touchHistory, gestureState._accountsForMovesUpTo); var movedAfter = gestureState._accountsForMovesUpTo, prevX = previousCentroidXOfTouchesChangedAfter(touchHistory, movedAfter), x = currentCentroidXOfTouchesChangedAfter(touchHistory, movedAfter), prevY = previousCentroidYOfTouchesChangedAfter(touchHistory, movedAfter), y = currentCentroidYOfTouchesChangedAfter(touchHistory, movedAfter), nextDX = gestureState.dx + (x - prevX), nextDY = gestureState.dy + (y - prevY), dt = touchHistory.mostRecentTimeStamp - gestureState._accountsForMovesUpTo; gestureState.vx = (nextDX - gestureState.dx) / dt, gestureState.vy = (nextDY - gestureState.dy) / dt, gestureState.dx = nextDX, gestureState.dy = nextDY, gestureState._accountsForMovesUpTo = touchHistory.mostRecentTimeStamp; }, /** * @param {object} config Enhanced versions of all of the responder callbacks * that provide not only the typical `ResponderSyntheticEvent`, but also the * `PanResponder` gesture state. Simply replace the word `Responder` with * `PanResponder` in each of the typical `onResponder*` callbacks. For * example, the `config` object would look like: * * - `onMoveShouldSetPanResponder: (e, gestureState) => {...}` * - `onMoveShouldSetPanResponderCapture: (e, gestureState) => {...}` * - `onStartShouldSetPanResponder: (e, gestureState) => {...}` * - `onStartShouldSetPanResponderCapture: (e, gestureState) => {...}` * - `onPanResponderReject: (e, gestureState) => {...}` * - `onPanResponderGrant: (e, gestureState) => {...}` * - `onPanResponderStart: (e, gestureState) => {...}` * - `onPanResponderEnd: (e, gestureState) => {...}` * - `onPanResponderRelease: (e, gestureState) => {...}` * - `onPanResponderMove: (e, gestureState) => {...}` * - `onPanResponderTerminate: (e, gestureState) => {...}` * - `onPanResponderTerminationRequest: (e, gestureState) => {...}` * - `onShouldBlockNativeResponder: (e, gestureState) => {...}` * * In general, for events that have capture equivalents, we update the * gestureState once in the capture phase and can use it in the bubble phase * as well. * * Be careful with onStartShould* callbacks. They only reflect updated * `gestureState` for start/end events that bubble/capture to the Node. * Once the node is the responder, you can rely on every start/end event * being processed by the gesture and `gestureState` being updated * accordingly. (numberActiveTouches) may not be totally accurate unless you * are the responder. */ create(config) { var interactionState = { handle: null, shouldCancelClick: !1, timeout: null }, gestureState = { // Useful for debugging stateID: Math.random(), moveX: 0, moveY: 0, x0: 0, y0: 0, dx: 0, dy: 0, vx: 0, vy: 0, numberActiveTouches: 0, _accountsForMovesUpTo: 0 }, panHandlers = { onStartShouldSetResponder(event) { return config.onStartShouldSetPanResponder == null ? !1 : config.onStartShouldSetPanResponder(event, gestureState); }, onMoveShouldSetResponder(event) { return config.onMoveShouldSetPanResponder == null ? !1 : config.onMoveShouldSetPanResponder(event, gestureState); }, onStartShouldSetResponderCapture(event) { return event.nativeEvent.touches.length === 1 && PanResponder._initializeGestureState(gestureState), gestureState.numberActiveTouches = event.touchHistory.numberActiveTouches, config.onStartShouldSetPanResponderCapture != null ? config.onStartShouldSetPanResponderCapture(event, gestureState) : !1; }, onMoveShouldSetResponderCapture(event) { var touchHistory = event.touchHistory; return gestureState._accountsForMovesUpTo === touchHistory.mostRecentTimeStamp ? !1 : (PanResponder._updateGestureStateOnMove(gestureState, touchHistory), config.onMoveShouldSetPanResponderCapture ? config.onMoveShouldSetPanResponderCapture(event, gestureState) : !1); }, onResponderGrant(event) { return interactionState.handle || (interactionState.handle = InteractionManager.createInteractionHandle()), interactionState.timeout && clearInteractionTimeout(interactionState), interactionState.shouldCancelClick = !0, gestureState.x0 = currentCentroidX(event.touchHistory), gestureState.y0 = currentCentroidY(event.touchHistory), gestureState.dx = 0, gestureState.dy = 0, config.onPanResponderGrant && config.onPanResponderGrant(event, gestureState), config.onShouldBlockNativeResponder == null ? !0 : config.onShouldBlockNativeResponder(event, gestureState); }, onResponderReject(event) { clearInteractionHandle(interactionState, // @ts-ignore config.onPanResponderReject, event, gestureState); }, onResponderRelease(event) { clearInteractionHandle(interactionState, config.onPanResponderRelease, event, gestureState), setInteractionTimeout(interactionState), PanResponder._initializeGestureState(gestureState); }, onResponderStart(event) { var touchHistory = event.touchHistory; gestureState.numberActiveTouches = touchHistory.numberActiveTouches, config.onPanResponderStart && config.onPanResponderStart(event, gestureState); }, onResponderMove(event) { var touchHistory = event.touchHistory; gestureState._accountsForMovesUpTo !== touchHistory.mostRecentTimeStamp && (PanResponder._updateGestureStateOnMove(gestureState, touchHistory), config.onPanResponderMove && config.onPanResponderMove(event, gestureState)); }, onResponderEnd(event) { var touchHistory = event.touchHistory; gestureState.numberActiveTouches = touchHistory.numberActiveTouches, clearInteractionHandle(interactionState, config.onPanResponderEnd, event, gestureState); }, onResponderTerminate(event) { clearInteractionHandle(interactionState, // @ts-ignore config.onPanResponderTerminate, event, gestureState), setInteractionTimeout(interactionState), PanResponder._initializeGestureState(gestureState); }, onResponderTerminationRequest(event) { return config.onPanResponderTerminationRequest == null ? !0 : config.onPanResponderTerminationRequest(event, gestureState); }, // We do not want to trigger 'click' activated gestures or native behaviors // on any pan target that is under a mouse cursor when it is released. // Browsers will natively cancel 'click' events on a target if a non-mouse // active pointer moves. onClickCapture: function (event) { interactionState.shouldCancelClick === !0 && (event.stopPropagation(), event.preventDefault()); } }; return { panHandlers, getInteractionHandle() { return interactionState.handle; } }; } }; function clearInteractionHandle(interactionState, callback, event, gestureState) { interactionState.handle && (InteractionManager.clearInteractionHandle(interactionState.handle), interactionState.handle = null), callback && callback(event, gestureState); } function clearInteractionTimeout(interactionState) { clearTimeout(interactionState.timeout); } function setInteractionTimeout(interactionState) { interactionState.timeout = setTimeout(function () { interactionState.shouldCancelClick = !1; }, 250); } var PanResponder_default = PanResponder; export { PanResponder_default as default }; //# sourceMappingURL=index.native.js.map