/** * Copyright (c) Facebook, Inc. and its affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * * * @format */ /** * PAN RESPONDER * * `PanResponder` uses the Responder System to reconcile several touches into * a single gesture. It makes single-touch gestures resilient to extra touches, * and can be used to recognize simple multi-touch gestures. For each handler, * it provides a `gestureState` object alongside the ResponderEvent object. * * By default, `PanResponder` holds an `InteractionManager` handle to block * long-running JS events from interrupting active gestures. * * 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. | * | | * | | * +--------------------------------+ */ 'use strict'; import InteractionManager from '../InteractionManager'; import TouchHistoryMath from '../../vendor/react-native/TouchHistoryMath'; var currentCentroidX = TouchHistoryMath.currentCentroidX, currentCentroidY = TouchHistoryMath.currentCentroidY, currentCentroidXOfTouchesChangedAfter = TouchHistoryMath.currentCentroidXOfTouchesChangedAfter, currentCentroidYOfTouchesChangedAfter = TouchHistoryMath.currentCentroidYOfTouchesChangedAfter, previousCentroidXOfTouchesChangedAfter = TouchHistoryMath.previousCentroidXOfTouchesChangedAfter, previousCentroidYOfTouchesChangedAfter = TouchHistoryMath.previousCentroidYOfTouchesChangedAfter; var PanResponder = { _initializeGestureState: function _initializeGestureState(gestureState) { gestureState.x = 0; gestureState.y = 0; gestureState.initialX = 0; gestureState.initialY = 0; gestureState.deltaX = 0; gestureState.deltaY = 0; gestureState.velocityX = 0; gestureState.velocityY = 0; gestureState.numberActiveTouches = 0; // All `gestureState` accounts for timeStamps up until: gestureState._accountsForMovesUpTo = 0; }, /** * Take all recently moved touches, 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. 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. 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. * * 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. * * x/y: If a move event has been observed, `(x, y)` is the centroid of the most * recently moved "cluster" of active touches. * deltaX/deltaY: Cumulative 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). */ _updateGestureStateOnMove: function _updateGestureStateOnMove(gestureState, touchHistory) { var movedAfter = gestureState._accountsForMovesUpTo; var prevX = previousCentroidXOfTouchesChangedAfter(touchHistory, movedAfter); var prevY = previousCentroidYOfTouchesChangedAfter(touchHistory, movedAfter); var prevDeltaX = gestureState.deltaX; var prevDeltaY = gestureState.deltaY; var x = currentCentroidXOfTouchesChangedAfter(touchHistory, movedAfter); var y = currentCentroidYOfTouchesChangedAfter(touchHistory, movedAfter); var deltaX = prevDeltaX + (x - prevX); var deltaY = prevDeltaY + (y - prevY); // TODO: This must be filtered intelligently. var dt = touchHistory.mostRecentTimeStamp - gestureState._accountsForMovesUpTo; gestureState.deltaX = deltaX; gestureState.deltaY = deltaY; gestureState.numberActiveTouches = touchHistory.numberActiveTouches; gestureState.velocityX = (deltaX - prevDeltaX) / dt; gestureState.velocityY = (deltaY - prevDeltaY) / dt; gestureState.x = x; gestureState.y = y; gestureState._accountsForMovesUpTo = touchHistory.mostRecentTimeStamp; }, /** * Enhanced versions of all of the responder callbacks that provide not only * the `ResponderEvent`, but also the `PanResponder` gesture state. * * 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. */ create: function create(config) { var interactionState = { handle: null }; var gestureState = { // Useful for debugging stateID: Math.random(), x: 0, y: 0, initialX: 0, initialY: 0, deltaX: 0, deltaY: 0, velocityX: 0, velocityY: 0, numberActiveTouches: 0, _accountsForMovesUpTo: 0 }; var _onStartShouldSetResponder = config.onStartShouldSetResponder, _onStartShouldSetResponderCapture = config.onStartShouldSetResponderCapture, _onMoveShouldSetResponder = config.onMoveShouldSetResponder, _onMoveShouldSetResponderCapture = config.onMoveShouldSetResponderCapture, onPanGrant = config.onPanGrant, onPanStart = config.onPanStart, onPanMove = config.onPanMove, onPanEnd = config.onPanEnd, onPanRelease = config.onPanRelease, onPanReject = config.onPanReject, onPanTerminate = config.onPanTerminate, onPanTerminationRequest = config.onPanTerminationRequest; var panHandlers = { onStartShouldSetResponder: function onStartShouldSetResponder(event) { return _onStartShouldSetResponder != null ? _onStartShouldSetResponder(event, gestureState) : false; }, onMoveShouldSetResponder: function onMoveShouldSetResponder(event) { return _onMoveShouldSetResponder != null ? _onMoveShouldSetResponder(event, gestureState) : false; }, onStartShouldSetResponderCapture: function onStartShouldSetResponderCapture(event) { // TODO: Actually, we should reinitialize the state any time // touches.length increases from 0 active to > 0 active. if (event.nativeEvent.touches.length === 1) { PanResponder._initializeGestureState(gestureState); } gestureState.numberActiveTouches = event.touchHistory.numberActiveTouches; return _onStartShouldSetResponderCapture != null ? _onStartShouldSetResponderCapture(event, gestureState) : false; }, onMoveShouldSetResponderCapture: function onMoveShouldSetResponderCapture(event) { var touchHistory = event.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. // NOTE: commented out because new responder system should get it right. //if (gestureState._accountsForMovesUpTo === touchHistory.mostRecentTimeStamp) { // return false; //} PanResponder._updateGestureStateOnMove(gestureState, touchHistory); return _onMoveShouldSetResponderCapture != null ? _onMoveShouldSetResponderCapture(event, gestureState) : false; }, onResponderGrant: function onResponderGrant(event) { if (!interactionState.handle) { interactionState.handle = InteractionManager.createInteractionHandle(); } gestureState.initialX = currentCentroidX(event.touchHistory); gestureState.initialY = currentCentroidY(event.touchHistory); gestureState.deltaX = 0; gestureState.deltaY = 0; if (onPanGrant != null) { onPanGrant(event, gestureState); } }, onResponderReject: function onResponderReject(event) { clearInteractionHandle(interactionState, onPanReject, event, gestureState); }, onResponderStart: function onResponderStart(event) { var numberActiveTouches = event.touchHistory.numberActiveTouches; gestureState.numberActiveTouches = numberActiveTouches; if (onPanStart != null) { onPanStart(event, gestureState); } }, onResponderMove: function onResponderMove(event) { var touchHistory = event.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. PanResponder._updateGestureStateOnMove(gestureState, touchHistory); if (onPanMove != null) { onPanMove(event, gestureState); } }, onResponderEnd: function onResponderEnd(event) { var numberActiveTouches = event.touchHistory.numberActiveTouches; gestureState.numberActiveTouches = numberActiveTouches; clearInteractionHandle(interactionState, onPanEnd, event, gestureState); }, onResponderRelease: function onResponderRelease(event) { clearInteractionHandle(interactionState, onPanRelease, event, gestureState); PanResponder._initializeGestureState(gestureState); }, onResponderTerminate: function onResponderTerminate(event) { clearInteractionHandle(interactionState, onPanTerminate, event, gestureState); PanResponder._initializeGestureState(gestureState); }, onResponderTerminationRequest: function onResponderTerminationRequest(event) { return onPanTerminationRequest != null ? onPanTerminationRequest(event, gestureState) : true; } }; return { panHandlers: panHandlers, getInteractionHandle: function getInteractionHandle() { return interactionState.handle; } }; } }; function clearInteractionHandle(interactionState, callback, event, gestureState) { if (interactionState.handle) { InteractionManager.clearInteractionHandle(interactionState.handle); interactionState.handle = null; } if (callback) { callback(event, gestureState); } } export default PanResponder;