main
js 237 lines 7.25 KB
Raw
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 as 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 // $FlowFixMe[incompatible-type] might be null according to type
211 nativeEvent.touches[0].identifier;
212 }
213 } else if (isEndish(topLevelType)) {
214 nativeEvent.changedTouches.forEach(recordTouchEnd);
215 touchHistory.numberActiveTouches = nativeEvent.touches.length;
216 if (touchHistory.numberActiveTouches === 1) {
217 for (let i = 0; i < touchBank.length; i++) {
218 const touchTrackToCheck = touchBank[i];
219 if (touchTrackToCheck != null && touchTrackToCheck.touchActive) {
220 touchHistory.indexOfSingleActiveTouch = i;
221 break;
222 }
223 }
224 if (__DEV__) {
225 const activeRecord = touchBank[touchHistory.indexOfSingleActiveTouch];
226 if (activeRecord == null || !activeRecord.touchActive) {
227 console.error('Cannot find single active touch.');
228 }
229 }
230 }
231 }
232 },
233
234 touchHistory,
235 };
236
237 export default ResponderTouchHistoryStore;