123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581 |
- /**
- * 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.
- *
- * @flow
- * @format
- */
- 'use strict';
- const InteractionManager = require('./InteractionManager');
- const TouchHistoryMath = require('./TouchHistoryMath');
- import type {PressEvent} from '../Types/CoreEventTypes';
- const currentCentroidXOfTouchesChangedAfter =
- TouchHistoryMath.currentCentroidXOfTouchesChangedAfter;
- const currentCentroidYOfTouchesChangedAfter =
- TouchHistoryMath.currentCentroidYOfTouchesChangedAfter;
- const previousCentroidXOfTouchesChangedAfter =
- TouchHistoryMath.previousCentroidXOfTouchesChangedAfter;
- const previousCentroidYOfTouchesChangedAfter =
- TouchHistoryMath.previousCentroidYOfTouchesChangedAfter;
- const currentCentroidX = TouchHistoryMath.currentCentroidX;
- const currentCentroidY = TouchHistoryMath.currentCentroidY;
- /**
- * `PanResponder` reconciles 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.
- *
- * By default, `PanResponder` holds an `InteractionManager` handle to block
- * long-running JS events from interrupting active gestures.
- *
- * It provides a predictable wrapper of the responder handlers provided by the
- * [gesture responder system](docs/gesture-responder-system.html).
- * For each handler, it provides a new `gestureState` object alongside the
- * native event object:
- *
- * ```
- * onPanResponderMove: (event, gestureState) => {}
- * ```
- *
- * A native event is a synthetic touch event with the following form:
- *
- * - `nativeEvent`
- * + `changedTouches` - Array of all touch events that have changed since the last event
- * + `identifier` - The ID of the touch
- * + `locationX` - The X position of the touch, relative to the element
- * + `locationY` - The Y position of the touch, relative to the element
- * + `pageX` - The X position of the touch, relative to the root element
- * + `pageY` - The Y position of the touch, relative to the root element
- * + `target` - The node id of the element receiving the touch event
- * + `timestamp` - A time identifier for the touch, useful for velocity calculation
- * + `touches` - Array of all current touches on the screen
- *
- * A `gestureState` object has the following:
- *
- * - `stateID` - ID of the gestureState- persisted as long as there at least
- * one touch on screen
- * - `moveX` - the latest screen coordinates of the recently-moved touch
- * - `moveY` - the latest screen coordinates of the recently-moved touch
- * - `x0` - the screen coordinates of the responder grant
- * - `y0` - the screen coordinates of the responder grant
- * - `dx` - accumulated distance of the gesture since the touch started
- * - `dy` - accumulated distance of the gesture since the touch started
- * - `vx` - current velocity of the gesture
- * - `vy` - current velocity of the gesture
- * - `numberActiveTouches` - Number of touches currently on screen
- *
- * ### Basic Usage
- *
- * ```
- * componentWillMount: function() {
- * this._panResponder = PanResponder.create({
- * // Ask to be the responder:
- * onStartShouldSetPanResponder: (evt, gestureState) => true,
- * onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
- * onMoveShouldSetPanResponder: (evt, gestureState) => true,
- * onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
- *
- * onPanResponderGrant: (evt, gestureState) => {
- * // The gesture has started. Show visual feedback so the user knows
- * // what is happening!
- *
- * // gestureState.d{x,y} will be set to zero now
- * },
- * onPanResponderMove: (evt, gestureState) => {
- * // The most recent move distance is gestureState.move{X,Y}
- *
- * // The accumulated gesture distance since becoming responder is
- * // gestureState.d{x,y}
- * },
- * onPanResponderTerminationRequest: (evt, gestureState) => true,
- * onPanResponderRelease: (evt, gestureState) => {
- * // The user has released all touches while this view is the
- * // responder. This typically means a gesture has succeeded
- * },
- * onPanResponderTerminate: (evt, gestureState) => {
- * // Another component has become the responder, so this gesture
- * // should be cancelled
- * },
- * onShouldBlockNativeResponder: (evt, gestureState) => {
- * // Returns whether this component should block native components from becoming the JS
- * // responder. Returns true by default. Is currently only supported on android.
- * return true;
- * },
- * });
- * },
- *
- * render: function() {
- * return (
- * <View {...this._panResponder.panHandlers} />
- * );
- * },
- *
- * ```
- *
- * ### Working Example
- *
- * To see it in action, try the
- * [PanResponder example in RNTester](https://github.com/facebook/react-native/blob/master/RNTester/js/PanResponderExample.js)
- */
- export type GestureState = {|
- /**
- * ID of the gestureState - persisted as long as there at least one touch on screen
- */
- stateID: number,
- /**
- * The latest screen coordinates of the recently-moved touch
- */
- moveX: number,
- /**
- * The latest screen coordinates of the recently-moved touch
- */
- moveY: number,
- /**
- * The screen coordinates of the responder grant
- */
- x0: number,
- /**
- * The screen coordinates of the responder grant
- */
- y0: number,
- /**
- * Accumulated distance of the gesture since the touch started
- */
- dx: number,
- /**
- * Accumulated distance of the gesture since the touch started
- */
- dy: number,
- /**
- * Current velocity of the gesture
- */
- vx: number,
- /**
- * Current velocity of the gesture
- */
- vy: number,
- /**
- * Number of touches currently on screen
- */
- numberActiveTouches: number,
- /**
- * All `gestureState` accounts for timeStamps up until this value
- *
- * @private
- */
- _accountsForMovesUpTo: number,
- |};
- type ActiveCallback = (
- event: PressEvent,
- gestureState: GestureState,
- ) => boolean;
- type PassiveCallback = (event: PressEvent, gestureState: GestureState) => mixed;
- type PanResponderConfig = $ReadOnly<{|
- onMoveShouldSetPanResponder?: ?ActiveCallback,
- onMoveShouldSetPanResponderCapture?: ?ActiveCallback,
- onStartShouldSetPanResponder?: ?ActiveCallback,
- onStartShouldSetPanResponderCapture?: ?ActiveCallback,
- /**
- * The body of `onResponderGrant` returns a bool, but the vast majority of
- * callsites return void and this TODO notice is found in it:
- * TODO: t7467124 investigate if this can be removed
- */
- onPanResponderGrant?: ?(PassiveCallback | ActiveCallback),
- onPanResponderReject?: ?PassiveCallback,
- onPanResponderStart?: ?PassiveCallback,
- onPanResponderEnd?: ?PassiveCallback,
- onPanResponderRelease?: ?PassiveCallback,
- onPanResponderMove?: ?PassiveCallback,
- onPanResponderTerminate?: ?PassiveCallback,
- onPanResponderTerminationRequest?: ?ActiveCallback,
- onShouldBlockNativeResponder?: ?ActiveCallback,
- |}>;
- const 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) {
- 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;
- },
- /**
- * 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: GestureState,
- touchHistory: $PropertyType<PressEvent, 'touchHistory'>,
- ) {
- gestureState.numberActiveTouches = touchHistory.numberActiveTouches;
- gestureState.moveX = currentCentroidXOfTouchesChangedAfter(
- touchHistory,
- gestureState._accountsForMovesUpTo,
- );
- gestureState.moveY = currentCentroidYOfTouchesChangedAfter(
- touchHistory,
- gestureState._accountsForMovesUpTo,
- );
- 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 nextDX = gestureState.dx + (x - prevX);
- const nextDY = gestureState.dy + (y - prevY);
- // TODO: This must be filtered intelligently.
- const 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: PanResponderConfig,
- ): $TEMPORARY$object<{|
- getInteractionHandle: () => ?number,
- panHandlers: $TEMPORARY$object<{|
- onMoveShouldSetResponder: (event: PressEvent) => boolean,
- onMoveShouldSetResponderCapture: (event: PressEvent) => boolean,
- onResponderEnd: (event: PressEvent) => void,
- onResponderGrant: (event: PressEvent) => boolean,
- onResponderMove: (event: PressEvent) => void,
- onResponderReject: (event: PressEvent) => void,
- onResponderRelease: (event: PressEvent) => void,
- onResponderStart: (event: PressEvent) => void,
- onResponderTerminate: (event: PressEvent) => void,
- onResponderTerminationRequest: (event: PressEvent) => boolean,
- onStartShouldSetResponder: (event: PressEvent) => boolean,
- onStartShouldSetResponderCapture: (event: PressEvent) => boolean,
- |}>,
- |}> {
- const interactionState = {
- handle: (null: ?number),
- };
- const gestureState: 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,
- };
- const panHandlers = {
- onStartShouldSetResponder(event: PressEvent): boolean {
- return config.onStartShouldSetPanResponder == null
- ? false
- : config.onStartShouldSetPanResponder(event, gestureState);
- },
- onMoveShouldSetResponder(event: PressEvent): boolean {
- return config.onMoveShouldSetPanResponder == null
- ? false
- : config.onMoveShouldSetPanResponder(event, gestureState);
- },
- onStartShouldSetResponderCapture(event: PressEvent): boolean {
- // 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 config.onStartShouldSetPanResponderCapture != null
- ? config.onStartShouldSetPanResponderCapture(event, gestureState)
- : false;
- },
- onMoveShouldSetResponderCapture(event: PressEvent): boolean {
- const 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.
- if (
- gestureState._accountsForMovesUpTo ===
- touchHistory.mostRecentTimeStamp
- ) {
- return false;
- }
- PanResponder._updateGestureStateOnMove(gestureState, touchHistory);
- return config.onMoveShouldSetPanResponderCapture
- ? config.onMoveShouldSetPanResponderCapture(event, gestureState)
- : false;
- },
- onResponderGrant(event: PressEvent): boolean {
- if (!interactionState.handle) {
- interactionState.handle = InteractionManager.createInteractionHandle();
- }
- gestureState.x0 = currentCentroidX(event.touchHistory);
- gestureState.y0 = currentCentroidY(event.touchHistory);
- gestureState.dx = 0;
- gestureState.dy = 0;
- if (config.onPanResponderGrant) {
- config.onPanResponderGrant(event, gestureState);
- }
- // TODO: t7467124 investigate if this can be removed
- return config.onShouldBlockNativeResponder == null
- ? true
- : config.onShouldBlockNativeResponder(event, gestureState);
- },
- onResponderReject(event: PressEvent): void {
- clearInteractionHandle(
- interactionState,
- config.onPanResponderReject,
- event,
- gestureState,
- );
- },
- onResponderRelease(event: PressEvent): void {
- clearInteractionHandle(
- interactionState,
- config.onPanResponderRelease,
- event,
- gestureState,
- );
- PanResponder._initializeGestureState(gestureState);
- },
- onResponderStart(event: PressEvent): void {
- const touchHistory = event.touchHistory;
- gestureState.numberActiveTouches = touchHistory.numberActiveTouches;
- if (config.onPanResponderStart) {
- config.onPanResponderStart(event, gestureState);
- }
- },
- onResponderMove(event: PressEvent): void {
- const 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 (config.onPanResponderMove) {
- config.onPanResponderMove(event, gestureState);
- }
- },
- onResponderEnd(event: PressEvent): void {
- const touchHistory = event.touchHistory;
- gestureState.numberActiveTouches = touchHistory.numberActiveTouches;
- clearInteractionHandle(
- interactionState,
- config.onPanResponderEnd,
- event,
- gestureState,
- );
- },
- onResponderTerminate(event: PressEvent): void {
- clearInteractionHandle(
- interactionState,
- config.onPanResponderTerminate,
- event,
- gestureState,
- );
- PanResponder._initializeGestureState(gestureState);
- },
- onResponderTerminationRequest(event: PressEvent): boolean {
- return config.onPanResponderTerminationRequest == null
- ? true
- : config.onPanResponderTerminationRequest(event, gestureState);
- },
- };
- return {
- panHandlers,
- getInteractionHandle(): ?number {
- return interactionState.handle;
- },
- };
- },
- };
- function clearInteractionHandle(
- interactionState: {handle: ?number, ...},
- callback: ?(ActiveCallback | PassiveCallback),
- event: PressEvent,
- gestureState: GestureState,
- ) {
- if (interactionState.handle) {
- InteractionManager.clearInteractionHandle(interactionState.handle);
- interactionState.handle = null;
- }
- if (callback) {
- callback(event, gestureState);
- }
- }
- export type PanResponderInstance = $Call<
- $PropertyType<typeof PanResponder, 'create'>,
- PanResponderConfig,
- >;
- module.exports = PanResponder;
|