PanResponder.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581
  1. /**
  2. * Copyright (c) Facebook, Inc. and its affiliates.
  3. *
  4. * This source code is licensed under the MIT license found in the
  5. * LICENSE file in the root directory of this source tree.
  6. *
  7. * @flow
  8. * @format
  9. */
  10. 'use strict';
  11. const InteractionManager = require('./InteractionManager');
  12. const TouchHistoryMath = require('./TouchHistoryMath');
  13. import type {PressEvent} from '../Types/CoreEventTypes';
  14. const currentCentroidXOfTouchesChangedAfter =
  15. TouchHistoryMath.currentCentroidXOfTouchesChangedAfter;
  16. const currentCentroidYOfTouchesChangedAfter =
  17. TouchHistoryMath.currentCentroidYOfTouchesChangedAfter;
  18. const previousCentroidXOfTouchesChangedAfter =
  19. TouchHistoryMath.previousCentroidXOfTouchesChangedAfter;
  20. const previousCentroidYOfTouchesChangedAfter =
  21. TouchHistoryMath.previousCentroidYOfTouchesChangedAfter;
  22. const currentCentroidX = TouchHistoryMath.currentCentroidX;
  23. const currentCentroidY = TouchHistoryMath.currentCentroidY;
  24. /**
  25. * `PanResponder` reconciles several touches into a single gesture. It makes
  26. * single-touch gestures resilient to extra touches, and can be used to
  27. * recognize simple multi-touch gestures.
  28. *
  29. * By default, `PanResponder` holds an `InteractionManager` handle to block
  30. * long-running JS events from interrupting active gestures.
  31. *
  32. * It provides a predictable wrapper of the responder handlers provided by the
  33. * [gesture responder system](docs/gesture-responder-system.html).
  34. * For each handler, it provides a new `gestureState` object alongside the
  35. * native event object:
  36. *
  37. * ```
  38. * onPanResponderMove: (event, gestureState) => {}
  39. * ```
  40. *
  41. * A native event is a synthetic touch event with the following form:
  42. *
  43. * - `nativeEvent`
  44. * + `changedTouches` - Array of all touch events that have changed since the last event
  45. * + `identifier` - The ID of the touch
  46. * + `locationX` - The X position of the touch, relative to the element
  47. * + `locationY` - The Y position of the touch, relative to the element
  48. * + `pageX` - The X position of the touch, relative to the root element
  49. * + `pageY` - The Y position of the touch, relative to the root element
  50. * + `target` - The node id of the element receiving the touch event
  51. * + `timestamp` - A time identifier for the touch, useful for velocity calculation
  52. * + `touches` - Array of all current touches on the screen
  53. *
  54. * A `gestureState` object has the following:
  55. *
  56. * - `stateID` - ID of the gestureState- persisted as long as there at least
  57. * one touch on screen
  58. * - `moveX` - the latest screen coordinates of the recently-moved touch
  59. * - `moveY` - the latest screen coordinates of the recently-moved touch
  60. * - `x0` - the screen coordinates of the responder grant
  61. * - `y0` - the screen coordinates of the responder grant
  62. * - `dx` - accumulated distance of the gesture since the touch started
  63. * - `dy` - accumulated distance of the gesture since the touch started
  64. * - `vx` - current velocity of the gesture
  65. * - `vy` - current velocity of the gesture
  66. * - `numberActiveTouches` - Number of touches currently on screen
  67. *
  68. * ### Basic Usage
  69. *
  70. * ```
  71. * componentWillMount: function() {
  72. * this._panResponder = PanResponder.create({
  73. * // Ask to be the responder:
  74. * onStartShouldSetPanResponder: (evt, gestureState) => true,
  75. * onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
  76. * onMoveShouldSetPanResponder: (evt, gestureState) => true,
  77. * onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,
  78. *
  79. * onPanResponderGrant: (evt, gestureState) => {
  80. * // The gesture has started. Show visual feedback so the user knows
  81. * // what is happening!
  82. *
  83. * // gestureState.d{x,y} will be set to zero now
  84. * },
  85. * onPanResponderMove: (evt, gestureState) => {
  86. * // The most recent move distance is gestureState.move{X,Y}
  87. *
  88. * // The accumulated gesture distance since becoming responder is
  89. * // gestureState.d{x,y}
  90. * },
  91. * onPanResponderTerminationRequest: (evt, gestureState) => true,
  92. * onPanResponderRelease: (evt, gestureState) => {
  93. * // The user has released all touches while this view is the
  94. * // responder. This typically means a gesture has succeeded
  95. * },
  96. * onPanResponderTerminate: (evt, gestureState) => {
  97. * // Another component has become the responder, so this gesture
  98. * // should be cancelled
  99. * },
  100. * onShouldBlockNativeResponder: (evt, gestureState) => {
  101. * // Returns whether this component should block native components from becoming the JS
  102. * // responder. Returns true by default. Is currently only supported on android.
  103. * return true;
  104. * },
  105. * });
  106. * },
  107. *
  108. * render: function() {
  109. * return (
  110. * <View {...this._panResponder.panHandlers} />
  111. * );
  112. * },
  113. *
  114. * ```
  115. *
  116. * ### Working Example
  117. *
  118. * To see it in action, try the
  119. * [PanResponder example in RNTester](https://github.com/facebook/react-native/blob/master/RNTester/js/PanResponderExample.js)
  120. */
  121. export type GestureState = {|
  122. /**
  123. * ID of the gestureState - persisted as long as there at least one touch on screen
  124. */
  125. stateID: number,
  126. /**
  127. * The latest screen coordinates of the recently-moved touch
  128. */
  129. moveX: number,
  130. /**
  131. * The latest screen coordinates of the recently-moved touch
  132. */
  133. moveY: number,
  134. /**
  135. * The screen coordinates of the responder grant
  136. */
  137. x0: number,
  138. /**
  139. * The screen coordinates of the responder grant
  140. */
  141. y0: number,
  142. /**
  143. * Accumulated distance of the gesture since the touch started
  144. */
  145. dx: number,
  146. /**
  147. * Accumulated distance of the gesture since the touch started
  148. */
  149. dy: number,
  150. /**
  151. * Current velocity of the gesture
  152. */
  153. vx: number,
  154. /**
  155. * Current velocity of the gesture
  156. */
  157. vy: number,
  158. /**
  159. * Number of touches currently on screen
  160. */
  161. numberActiveTouches: number,
  162. /**
  163. * All `gestureState` accounts for timeStamps up until this value
  164. *
  165. * @private
  166. */
  167. _accountsForMovesUpTo: number,
  168. |};
  169. type ActiveCallback = (
  170. event: PressEvent,
  171. gestureState: GestureState,
  172. ) => boolean;
  173. type PassiveCallback = (event: PressEvent, gestureState: GestureState) => mixed;
  174. type PanResponderConfig = $ReadOnly<{|
  175. onMoveShouldSetPanResponder?: ?ActiveCallback,
  176. onMoveShouldSetPanResponderCapture?: ?ActiveCallback,
  177. onStartShouldSetPanResponder?: ?ActiveCallback,
  178. onStartShouldSetPanResponderCapture?: ?ActiveCallback,
  179. /**
  180. * The body of `onResponderGrant` returns a bool, but the vast majority of
  181. * callsites return void and this TODO notice is found in it:
  182. * TODO: t7467124 investigate if this can be removed
  183. */
  184. onPanResponderGrant?: ?(PassiveCallback | ActiveCallback),
  185. onPanResponderReject?: ?PassiveCallback,
  186. onPanResponderStart?: ?PassiveCallback,
  187. onPanResponderEnd?: ?PassiveCallback,
  188. onPanResponderRelease?: ?PassiveCallback,
  189. onPanResponderMove?: ?PassiveCallback,
  190. onPanResponderTerminate?: ?PassiveCallback,
  191. onPanResponderTerminationRequest?: ?ActiveCallback,
  192. onShouldBlockNativeResponder?: ?ActiveCallback,
  193. |}>;
  194. const PanResponder = {
  195. /**
  196. *
  197. * A graphical explanation of the touch data flow:
  198. *
  199. * +----------------------------+ +--------------------------------+
  200. * | ResponderTouchHistoryStore | |TouchHistoryMath |
  201. * +----------------------------+ +----------+---------------------+
  202. * |Global store of touchHistory| |Allocation-less math util |
  203. * |including activeness, start | |on touch history (centroids |
  204. * |position, prev/cur position.| |and multitouch movement etc) |
  205. * | | | |
  206. * +----^-----------------------+ +----^---------------------------+
  207. * | |
  208. * | (records relevant history |
  209. * | of touches relevant for |
  210. * | implementing higher level |
  211. * | gestures) |
  212. * | |
  213. * +----+-----------------------+ +----|---------------------------+
  214. * | ResponderEventPlugin | | | Your App/Component |
  215. * +----------------------------+ +----|---------------------------+
  216. * |Negotiates which view gets | Low level | | High level |
  217. * |onResponderMove events. | events w/ | +-+-------+ events w/ |
  218. * |Also records history into | touchHistory| | Pan | multitouch + |
  219. * |ResponderTouchHistoryStore. +---------------->Responder+-----> accumulative|
  220. * +----------------------------+ attached to | | | distance and |
  221. * each event | +---------+ velocity. |
  222. * | |
  223. * | |
  224. * +--------------------------------+
  225. *
  226. *
  227. *
  228. * Gesture that calculates cumulative movement over time in a way that just
  229. * "does the right thing" for multiple touches. The "right thing" is very
  230. * nuanced. When moving two touches in opposite directions, the cumulative
  231. * distance is zero in each dimension. When two touches move in parallel five
  232. * pixels in the same direction, the cumulative distance is five, not ten. If
  233. * two touches start, one moves five in a direction, then stops and the other
  234. * touch moves fives in the same direction, the cumulative distance is ten.
  235. *
  236. * This logic requires a kind of processing of time "clusters" of touch events
  237. * so that two touch moves that essentially occur in parallel but move every
  238. * other frame respectively, are considered part of the same movement.
  239. *
  240. * Explanation of some of the non-obvious fields:
  241. *
  242. * - moveX/moveY: If no move event has been observed, then `(moveX, moveY)` is
  243. * invalid. If a move event has been observed, `(moveX, moveY)` is the
  244. * centroid of the most recently moved "cluster" of active touches.
  245. * (Currently all move have the same timeStamp, but later we should add some
  246. * threshold for what is considered to be "moving"). If a palm is
  247. * accidentally counted as a touch, but a finger is moving greatly, the palm
  248. * will move slightly, but we only want to count the single moving touch.
  249. * - x0/y0: Centroid location (non-cumulative) at the time of becoming
  250. * responder.
  251. * - dx/dy: Cumulative touch distance - not the same thing as sum of each touch
  252. * distance. Accounts for touch moves that are clustered together in time,
  253. * moving the same direction. Only valid when currently responder (otherwise,
  254. * it only represents the drag distance below the threshold).
  255. * - vx/vy: Velocity.
  256. */
  257. _initializeGestureState(gestureState: GestureState) {
  258. gestureState.moveX = 0;
  259. gestureState.moveY = 0;
  260. gestureState.x0 = 0;
  261. gestureState.y0 = 0;
  262. gestureState.dx = 0;
  263. gestureState.dy = 0;
  264. gestureState.vx = 0;
  265. gestureState.vy = 0;
  266. gestureState.numberActiveTouches = 0;
  267. // All `gestureState` accounts for timeStamps up until:
  268. gestureState._accountsForMovesUpTo = 0;
  269. },
  270. /**
  271. * This is nuanced and is necessary. It is incorrect to continuously take all
  272. * active *and* recently moved touches, find the centroid, and track how that
  273. * result changes over time. Instead, we must take all recently moved
  274. * touches, and calculate how the centroid has changed just for those
  275. * recently moved touches, and append that change to an accumulator. This is
  276. * to (at least) handle the case where the user is moving three fingers, and
  277. * then one of the fingers stops but the other two continue.
  278. *
  279. * This is very different than taking all of the recently moved touches and
  280. * storing their centroid as `dx/dy`. For correctness, we must *accumulate
  281. * changes* in the centroid of recently moved touches.
  282. *
  283. * There is also some nuance with how we handle multiple moved touches in a
  284. * single event. With the way `ReactNativeEventEmitter` dispatches touches as
  285. * individual events, multiple touches generate two 'move' events, each of
  286. * them triggering `onResponderMove`. But with the way `PanResponder` works,
  287. * all of the gesture inference is performed on the first dispatch, since it
  288. * looks at all of the touches (even the ones for which there hasn't been a
  289. * native dispatch yet). Therefore, `PanResponder` does not call
  290. * `onResponderMove` passed the first dispatch. This diverges from the
  291. * typical responder callback pattern (without using `PanResponder`), but
  292. * avoids more dispatches than necessary.
  293. */
  294. _updateGestureStateOnMove(
  295. gestureState: GestureState,
  296. touchHistory: $PropertyType<PressEvent, 'touchHistory'>,
  297. ) {
  298. gestureState.numberActiveTouches = touchHistory.numberActiveTouches;
  299. gestureState.moveX = currentCentroidXOfTouchesChangedAfter(
  300. touchHistory,
  301. gestureState._accountsForMovesUpTo,
  302. );
  303. gestureState.moveY = currentCentroidYOfTouchesChangedAfter(
  304. touchHistory,
  305. gestureState._accountsForMovesUpTo,
  306. );
  307. const movedAfter = gestureState._accountsForMovesUpTo;
  308. const prevX = previousCentroidXOfTouchesChangedAfter(
  309. touchHistory,
  310. movedAfter,
  311. );
  312. const x = currentCentroidXOfTouchesChangedAfter(touchHistory, movedAfter);
  313. const prevY = previousCentroidYOfTouchesChangedAfter(
  314. touchHistory,
  315. movedAfter,
  316. );
  317. const y = currentCentroidYOfTouchesChangedAfter(touchHistory, movedAfter);
  318. const nextDX = gestureState.dx + (x - prevX);
  319. const nextDY = gestureState.dy + (y - prevY);
  320. // TODO: This must be filtered intelligently.
  321. const dt =
  322. touchHistory.mostRecentTimeStamp - gestureState._accountsForMovesUpTo;
  323. gestureState.vx = (nextDX - gestureState.dx) / dt;
  324. gestureState.vy = (nextDY - gestureState.dy) / dt;
  325. gestureState.dx = nextDX;
  326. gestureState.dy = nextDY;
  327. gestureState._accountsForMovesUpTo = touchHistory.mostRecentTimeStamp;
  328. },
  329. /**
  330. * @param {object} config Enhanced versions of all of the responder callbacks
  331. * that provide not only the typical `ResponderSyntheticEvent`, but also the
  332. * `PanResponder` gesture state. Simply replace the word `Responder` with
  333. * `PanResponder` in each of the typical `onResponder*` callbacks. For
  334. * example, the `config` object would look like:
  335. *
  336. * - `onMoveShouldSetPanResponder: (e, gestureState) => {...}`
  337. * - `onMoveShouldSetPanResponderCapture: (e, gestureState) => {...}`
  338. * - `onStartShouldSetPanResponder: (e, gestureState) => {...}`
  339. * - `onStartShouldSetPanResponderCapture: (e, gestureState) => {...}`
  340. * - `onPanResponderReject: (e, gestureState) => {...}`
  341. * - `onPanResponderGrant: (e, gestureState) => {...}`
  342. * - `onPanResponderStart: (e, gestureState) => {...}`
  343. * - `onPanResponderEnd: (e, gestureState) => {...}`
  344. * - `onPanResponderRelease: (e, gestureState) => {...}`
  345. * - `onPanResponderMove: (e, gestureState) => {...}`
  346. * - `onPanResponderTerminate: (e, gestureState) => {...}`
  347. * - `onPanResponderTerminationRequest: (e, gestureState) => {...}`
  348. * - `onShouldBlockNativeResponder: (e, gestureState) => {...}`
  349. *
  350. * In general, for events that have capture equivalents, we update the
  351. * gestureState once in the capture phase and can use it in the bubble phase
  352. * as well.
  353. *
  354. * Be careful with onStartShould* callbacks. They only reflect updated
  355. * `gestureState` for start/end events that bubble/capture to the Node.
  356. * Once the node is the responder, you can rely on every start/end event
  357. * being processed by the gesture and `gestureState` being updated
  358. * accordingly. (numberActiveTouches) may not be totally accurate unless you
  359. * are the responder.
  360. */
  361. create(
  362. config: PanResponderConfig,
  363. ): $TEMPORARY$object<{|
  364. getInteractionHandle: () => ?number,
  365. panHandlers: $TEMPORARY$object<{|
  366. onMoveShouldSetResponder: (event: PressEvent) => boolean,
  367. onMoveShouldSetResponderCapture: (event: PressEvent) => boolean,
  368. onResponderEnd: (event: PressEvent) => void,
  369. onResponderGrant: (event: PressEvent) => boolean,
  370. onResponderMove: (event: PressEvent) => void,
  371. onResponderReject: (event: PressEvent) => void,
  372. onResponderRelease: (event: PressEvent) => void,
  373. onResponderStart: (event: PressEvent) => void,
  374. onResponderTerminate: (event: PressEvent) => void,
  375. onResponderTerminationRequest: (event: PressEvent) => boolean,
  376. onStartShouldSetResponder: (event: PressEvent) => boolean,
  377. onStartShouldSetResponderCapture: (event: PressEvent) => boolean,
  378. |}>,
  379. |}> {
  380. const interactionState = {
  381. handle: (null: ?number),
  382. };
  383. const gestureState: GestureState = {
  384. // Useful for debugging
  385. stateID: Math.random(),
  386. moveX: 0,
  387. moveY: 0,
  388. x0: 0,
  389. y0: 0,
  390. dx: 0,
  391. dy: 0,
  392. vx: 0,
  393. vy: 0,
  394. numberActiveTouches: 0,
  395. _accountsForMovesUpTo: 0,
  396. };
  397. const panHandlers = {
  398. onStartShouldSetResponder(event: PressEvent): boolean {
  399. return config.onStartShouldSetPanResponder == null
  400. ? false
  401. : config.onStartShouldSetPanResponder(event, gestureState);
  402. },
  403. onMoveShouldSetResponder(event: PressEvent): boolean {
  404. return config.onMoveShouldSetPanResponder == null
  405. ? false
  406. : config.onMoveShouldSetPanResponder(event, gestureState);
  407. },
  408. onStartShouldSetResponderCapture(event: PressEvent): boolean {
  409. // TODO: Actually, we should reinitialize the state any time
  410. // touches.length increases from 0 active to > 0 active.
  411. if (event.nativeEvent.touches.length === 1) {
  412. PanResponder._initializeGestureState(gestureState);
  413. }
  414. gestureState.numberActiveTouches =
  415. event.touchHistory.numberActiveTouches;
  416. return config.onStartShouldSetPanResponderCapture != null
  417. ? config.onStartShouldSetPanResponderCapture(event, gestureState)
  418. : false;
  419. },
  420. onMoveShouldSetResponderCapture(event: PressEvent): boolean {
  421. const touchHistory = event.touchHistory;
  422. // Responder system incorrectly dispatches should* to current responder
  423. // Filter out any touch moves past the first one - we would have
  424. // already processed multi-touch geometry during the first event.
  425. if (
  426. gestureState._accountsForMovesUpTo ===
  427. touchHistory.mostRecentTimeStamp
  428. ) {
  429. return false;
  430. }
  431. PanResponder._updateGestureStateOnMove(gestureState, touchHistory);
  432. return config.onMoveShouldSetPanResponderCapture
  433. ? config.onMoveShouldSetPanResponderCapture(event, gestureState)
  434. : false;
  435. },
  436. onResponderGrant(event: PressEvent): boolean {
  437. if (!interactionState.handle) {
  438. interactionState.handle = InteractionManager.createInteractionHandle();
  439. }
  440. gestureState.x0 = currentCentroidX(event.touchHistory);
  441. gestureState.y0 = currentCentroidY(event.touchHistory);
  442. gestureState.dx = 0;
  443. gestureState.dy = 0;
  444. if (config.onPanResponderGrant) {
  445. config.onPanResponderGrant(event, gestureState);
  446. }
  447. // TODO: t7467124 investigate if this can be removed
  448. return config.onShouldBlockNativeResponder == null
  449. ? true
  450. : config.onShouldBlockNativeResponder(event, gestureState);
  451. },
  452. onResponderReject(event: PressEvent): void {
  453. clearInteractionHandle(
  454. interactionState,
  455. config.onPanResponderReject,
  456. event,
  457. gestureState,
  458. );
  459. },
  460. onResponderRelease(event: PressEvent): void {
  461. clearInteractionHandle(
  462. interactionState,
  463. config.onPanResponderRelease,
  464. event,
  465. gestureState,
  466. );
  467. PanResponder._initializeGestureState(gestureState);
  468. },
  469. onResponderStart(event: PressEvent): void {
  470. const touchHistory = event.touchHistory;
  471. gestureState.numberActiveTouches = touchHistory.numberActiveTouches;
  472. if (config.onPanResponderStart) {
  473. config.onPanResponderStart(event, gestureState);
  474. }
  475. },
  476. onResponderMove(event: PressEvent): void {
  477. const touchHistory = event.touchHistory;
  478. // Guard against the dispatch of two touch moves when there are two
  479. // simultaneously changed touches.
  480. if (
  481. gestureState._accountsForMovesUpTo ===
  482. touchHistory.mostRecentTimeStamp
  483. ) {
  484. return;
  485. }
  486. // Filter out any touch moves past the first one - we would have
  487. // already processed multi-touch geometry during the first event.
  488. PanResponder._updateGestureStateOnMove(gestureState, touchHistory);
  489. if (config.onPanResponderMove) {
  490. config.onPanResponderMove(event, gestureState);
  491. }
  492. },
  493. onResponderEnd(event: PressEvent): void {
  494. const touchHistory = event.touchHistory;
  495. gestureState.numberActiveTouches = touchHistory.numberActiveTouches;
  496. clearInteractionHandle(
  497. interactionState,
  498. config.onPanResponderEnd,
  499. event,
  500. gestureState,
  501. );
  502. },
  503. onResponderTerminate(event: PressEvent): void {
  504. clearInteractionHandle(
  505. interactionState,
  506. config.onPanResponderTerminate,
  507. event,
  508. gestureState,
  509. );
  510. PanResponder._initializeGestureState(gestureState);
  511. },
  512. onResponderTerminationRequest(event: PressEvent): boolean {
  513. return config.onPanResponderTerminationRequest == null
  514. ? true
  515. : config.onPanResponderTerminationRequest(event, gestureState);
  516. },
  517. };
  518. return {
  519. panHandlers,
  520. getInteractionHandle(): ?number {
  521. return interactionState.handle;
  522. },
  523. };
  524. },
  525. };
  526. function clearInteractionHandle(
  527. interactionState: {handle: ?number, ...},
  528. callback: ?(ActiveCallback | PassiveCallback),
  529. event: PressEvent,
  530. gestureState: GestureState,
  531. ) {
  532. if (interactionState.handle) {
  533. InteractionManager.clearInteractionHandle(interactionState.handle);
  534. interactionState.handle = null;
  535. }
  536. if (callback) {
  537. callback(event, gestureState);
  538. }
  539. }
  540. export type PanResponderInstance = $Call<
  541. $PropertyType<typeof PanResponder, 'create'>,
  542. PanResponderConfig,
  543. >;
  544. module.exports = PanResponder;