diff --git a/Libraries/Interaction/PanResponder.flow.js b/Libraries/Interaction/PanResponder.flow.js new file mode 100644 index 00000000000..3db37a40612 --- /dev/null +++ b/Libraries/Interaction/PanResponder.flow.js @@ -0,0 +1,257 @@ +/** + * Copyright (c) Meta Platforms, Inc. and 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'; + +import type {PressEvent} from '../Types/CoreEventTypes'; + +/** + * `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 ( + * + * ); + * }, + * + * ``` + * + * ### Working Example + * + * To see it in action, try the + * [PanResponder example in RNTester](https://github.com/facebook/react-native/blob/HEAD/packages/rn-tester/js/examples/PanResponder/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 PanHandlers = {| + 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, +|}; + +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, +|}>; + +export type PanResponderType = { + _initializeGestureState: (gestureState: GestureState) => void, + _updateGestureStateOnMove: ( + gestureState: GestureState, + touchHistory: $PropertyType, + ) => void, + /** + * @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) => { + getInteractionHandle: () => ?number, + panHandlers: PanHandlers, + }, +}; diff --git a/Libraries/Interaction/PanResponder.js b/Libraries/Interaction/PanResponder.js index 8d2ae033bf4..03cb6aa036e 100644 --- a/Libraries/Interaction/PanResponder.js +++ b/Libraries/Interaction/PanResponder.js @@ -14,6 +14,7 @@ const InteractionManager = require('./InteractionManager'); const TouchHistoryMath = require('./TouchHistoryMath'); import type {PressEvent} from '../Types/CoreEventTypes'; +import type {PanResponderType} from './PanResponder.flow.js'; const currentCentroidXOfTouchesChangedAfter = TouchHistoryMath.currentCentroidXOfTouchesChangedAfter; @@ -226,7 +227,7 @@ type PanResponderConfig = $ReadOnly<{| onShouldBlockNativeResponder?: ?ActiveCallback, |}>; -const PanResponder = { +const PanResponder: PanResponderType = { /** * * A graphical explanation of the touch data flow: