1. /**
    
  2.  * Copyright (c) Meta Platforms, Inc. and 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.  */
    
  9. 
    
  10. import {isStartish, isMoveish, isEndish} from './ResponderTopLevelEventTypes';
    
  11. 
    
  12. /**
    
  13.  * Tracks the position and time of each active touch by `touch.identifier`. We
    
  14.  * should typically only see IDs in the range of 1-20 because IDs get recycled
    
  15.  * when touches end and start again.
    
  16.  */
    
  17. type TouchRecord = {
    
  18.   touchActive: boolean,
    
  19.   startPageX: number,
    
  20.   startPageY: number,
    
  21.   startTimeStamp: number,
    
  22.   currentPageX: number,
    
  23.   currentPageY: number,
    
  24.   currentTimeStamp: number,
    
  25.   previousPageX: number,
    
  26.   previousPageY: number,
    
  27.   previousTimeStamp: number,
    
  28. };
    
  29. 
    
  30. const MAX_TOUCH_BANK = 20;
    
  31. const touchBank: Array<TouchRecord> = [];
    
  32. const touchHistory = {
    
  33.   touchBank,
    
  34.   numberActiveTouches: 0,
    
  35.   // If there is only one active touch, we remember its location. This prevents
    
  36.   // us having to loop through all of the touches all the time in the most
    
  37.   // common case.
    
  38.   indexOfSingleActiveTouch: -1,
    
  39.   mostRecentTimeStamp: 0,
    
  40. };
    
  41. 
    
  42. type Touch = {
    
  43.   identifier: ?number,
    
  44.   pageX: number,
    
  45.   pageY: number,
    
  46.   timestamp: number,
    
  47.   ...
    
  48. };
    
  49. type TouchEvent = {
    
  50.   changedTouches: Array<Touch>,
    
  51.   touches: Array<Touch>,
    
  52.   ...
    
  53. };
    
  54. 
    
  55. function timestampForTouch(touch: Touch): number {
    
  56.   // The legacy internal implementation provides "timeStamp", which has been
    
  57.   // renamed to "timestamp". Let both work for now while we iron it out
    
  58.   // TODO (evv): rename timeStamp to timestamp in internal code
    
  59.   return (touch: any).timeStamp || touch.timestamp;
    
  60. }
    
  61. 
    
  62. /**
    
  63.  * TODO: Instead of making gestures recompute filtered velocity, we could
    
  64.  * include a built in velocity computation that can be reused globally.
    
  65.  */
    
  66. function createTouchRecord(touch: Touch): TouchRecord {
    
  67.   return {
    
  68.     touchActive: true,
    
  69.     startPageX: touch.pageX,
    
  70.     startPageY: touch.pageY,
    
  71.     startTimeStamp: timestampForTouch(touch),
    
  72.     currentPageX: touch.pageX,
    
  73.     currentPageY: touch.pageY,
    
  74.     currentTimeStamp: timestampForTouch(touch),
    
  75.     previousPageX: touch.pageX,
    
  76.     previousPageY: touch.pageY,
    
  77.     previousTimeStamp: timestampForTouch(touch),
    
  78.   };
    
  79. }
    
  80. 
    
  81. function resetTouchRecord(touchRecord: TouchRecord, touch: Touch): void {
    
  82.   touchRecord.touchActive = true;
    
  83.   touchRecord.startPageX = touch.pageX;
    
  84.   touchRecord.startPageY = touch.pageY;
    
  85.   touchRecord.startTimeStamp = timestampForTouch(touch);
    
  86.   touchRecord.currentPageX = touch.pageX;
    
  87.   touchRecord.currentPageY = touch.pageY;
    
  88.   touchRecord.currentTimeStamp = timestampForTouch(touch);
    
  89.   touchRecord.previousPageX = touch.pageX;
    
  90.   touchRecord.previousPageY = touch.pageY;
    
  91.   touchRecord.previousTimeStamp = timestampForTouch(touch);
    
  92. }
    
  93. 
    
  94. function getTouchIdentifier({identifier}: Touch): number {
    
  95.   if (identifier == null) {
    
  96.     throw new Error('Touch object is missing identifier.');
    
  97.   }
    
  98. 
    
  99.   if (__DEV__) {
    
  100.     if (identifier > MAX_TOUCH_BANK) {
    
  101.       console.error(
    
  102.         'Touch identifier %s is greater than maximum supported %s which causes ' +
    
  103.           'performance issues backfilling array locations for all of the indices.',
    
  104.         identifier,
    
  105.         MAX_TOUCH_BANK,
    
  106.       );
    
  107.     }
    
  108.   }
    
  109.   return identifier;
    
  110. }
    
  111. 
    
  112. function recordTouchStart(touch: Touch): void {
    
  113.   const identifier = getTouchIdentifier(touch);
    
  114.   const touchRecord = touchBank[identifier];
    
  115.   if (touchRecord) {
    
  116.     resetTouchRecord(touchRecord, touch);
    
  117.   } else {
    
  118.     touchBank[identifier] = createTouchRecord(touch);
    
  119.   }
    
  120.   touchHistory.mostRecentTimeStamp = timestampForTouch(touch);
    
  121. }
    
  122. 
    
  123. function recordTouchMove(touch: Touch): void {
    
  124.   const touchRecord = touchBank[getTouchIdentifier(touch)];
    
  125.   if (touchRecord) {
    
  126.     touchRecord.touchActive = true;
    
  127.     touchRecord.previousPageX = touchRecord.currentPageX;
    
  128.     touchRecord.previousPageY = touchRecord.currentPageY;
    
  129.     touchRecord.previousTimeStamp = touchRecord.currentTimeStamp;
    
  130.     touchRecord.currentPageX = touch.pageX;
    
  131.     touchRecord.currentPageY = touch.pageY;
    
  132.     touchRecord.currentTimeStamp = timestampForTouch(touch);
    
  133.     touchHistory.mostRecentTimeStamp = timestampForTouch(touch);
    
  134.   } else {
    
  135.     if (__DEV__) {
    
  136.       console.warn(
    
  137.         'Cannot record touch move without a touch start.\n' +
    
  138.           'Touch Move: %s\n' +
    
  139.           'Touch Bank: %s',
    
  140.         printTouch(touch),
    
  141.         printTouchBank(),
    
  142.       );
    
  143.     }
    
  144.   }
    
  145. }
    
  146. 
    
  147. function recordTouchEnd(touch: Touch): void {
    
  148.   const touchRecord = touchBank[getTouchIdentifier(touch)];
    
  149.   if (touchRecord) {
    
  150.     touchRecord.touchActive = false;
    
  151.     touchRecord.previousPageX = touchRecord.currentPageX;
    
  152.     touchRecord.previousPageY = touchRecord.currentPageY;
    
  153.     touchRecord.previousTimeStamp = touchRecord.currentTimeStamp;
    
  154.     touchRecord.currentPageX = touch.pageX;
    
  155.     touchRecord.currentPageY = touch.pageY;
    
  156.     touchRecord.currentTimeStamp = timestampForTouch(touch);
    
  157.     touchHistory.mostRecentTimeStamp = timestampForTouch(touch);
    
  158.   } else {
    
  159.     if (__DEV__) {
    
  160.       console.warn(
    
  161.         'Cannot record touch end without a touch start.\n' +
    
  162.           'Touch End: %s\n' +
    
  163.           'Touch Bank: %s',
    
  164.         printTouch(touch),
    
  165.         printTouchBank(),
    
  166.       );
    
  167.     }
    
  168.   }
    
  169. }
    
  170. 
    
  171. function printTouch(touch: Touch): string {
    
  172.   return JSON.stringify({
    
  173.     identifier: touch.identifier,
    
  174.     pageX: touch.pageX,
    
  175.     pageY: touch.pageY,
    
  176.     timestamp: timestampForTouch(touch),
    
  177.   });
    
  178. }
    
  179. 
    
  180. function printTouchBank(): string {
    
  181.   let printed = JSON.stringify(touchBank.slice(0, MAX_TOUCH_BANK));
    
  182.   if (touchBank.length > MAX_TOUCH_BANK) {
    
  183.     printed += ' (original size: ' + touchBank.length + ')';
    
  184.   }
    
  185.   return printed;
    
  186. }
    
  187. 
    
  188. let instrumentationCallback: ?(string, TouchEvent) => void;
    
  189. 
    
  190. const ResponderTouchHistoryStore = {
    
  191.   /**
    
  192.    * Registers a listener which can be used to instrument every touch event.
    
  193.    */
    
  194.   instrument(callback: (string, TouchEvent) => void): void {
    
  195.     instrumentationCallback = callback;
    
  196.   },
    
  197. 
    
  198.   recordTouchTrack(topLevelType: string, nativeEvent: TouchEvent): void {
    
  199.     if (instrumentationCallback != null) {
    
  200.       instrumentationCallback(topLevelType, nativeEvent);
    
  201.     }
    
  202. 
    
  203.     if (isMoveish(topLevelType)) {
    
  204.       nativeEvent.changedTouches.forEach(recordTouchMove);
    
  205.     } else if (isStartish(topLevelType)) {
    
  206.       nativeEvent.changedTouches.forEach(recordTouchStart);
    
  207.       touchHistory.numberActiveTouches = nativeEvent.touches.length;
    
  208.       if (touchHistory.numberActiveTouches === 1) {
    
  209.         touchHistory.indexOfSingleActiveTouch =
    
  210.           nativeEvent.touches[0].identifier;
    
  211.       }
    
  212.     } else if (isEndish(topLevelType)) {
    
  213.       nativeEvent.changedTouches.forEach(recordTouchEnd);
    
  214.       touchHistory.numberActiveTouches = nativeEvent.touches.length;
    
  215.       if (touchHistory.numberActiveTouches === 1) {
    
  216.         for (let i = 0; i < touchBank.length; i++) {
    
  217.           const touchTrackToCheck = touchBank[i];
    
  218.           if (touchTrackToCheck != null && touchTrackToCheck.touchActive) {
    
  219.             touchHistory.indexOfSingleActiveTouch = i;
    
  220.             break;
    
  221.           }
    
  222.         }
    
  223.         if (__DEV__) {
    
  224.           const activeRecord = touchBank[touchHistory.indexOfSingleActiveTouch];
    
  225.           if (activeRecord == null || !activeRecord.touchActive) {
    
  226.             console.error('Cannot find single active touch.');
    
  227.           }
    
  228.         }
    
  229.       }
    
  230.     }
    
  231.   },
    
  232. 
    
  233.   touchHistory,
    
  234. };
    
  235. 
    
  236. export default ResponderTouchHistoryStore;