UNPKG

15.3 kBPlain TextView Raw
1/* eslint-disable eslint-comments/no-unlimited-disable */
2/* eslint-disable */
3import Hammer from '@egjs/hammerjs';
4import { findNodeHandle } from 'react-native';
5
6import { State } from '../State';
7import { EventMap } from './constants';
8import * as NodeManager from './NodeManager';
9
10// TODO(TS) Replace with HammerInput if https://github.com/DefinitelyTyped/DefinitelyTyped/pull/50438/files is merged
11export type HammerInputExt = Omit<HammerInput, 'destroy' | 'handler' | 'init'>;
12
13export type Config = Partial<{
14 enabled: boolean;
15 minPointers: number;
16 maxPointers: number;
17 minDist: number;
18 minDistSq: number;
19 minVelocity: number;
20 minVelocitySq: number;
21 maxDist: number;
22 maxDistSq: number;
23 failOffsetXStart: number;
24 failOffsetYStart: number;
25 failOffsetXEnd: number;
26 failOffsetYEnd: number;
27 activeOffsetXStart: number;
28 activeOffsetXEnd: number;
29 activeOffsetYStart: number;
30 activeOffsetYEnd: number;
31 waitFor: any[] | null;
32}>;
33
34type NativeEvent = ReturnType<GestureHandler['transformEventData']>;
35
36let gestureInstances = 0;
37
38abstract class GestureHandler {
39 public handlerTag: any;
40 public isGestureRunning = false;
41 public view: number | null = null;
42 protected hasCustomActivationCriteria: boolean;
43 protected hasGestureFailed = false;
44 protected hammer: HammerManager | null = null;
45 protected initialRotation: number | null = null;
46 protected __initialX: any;
47 protected __initialY: any;
48 protected config: Config = {};
49 protected previousState: State = State.UNDETERMINED;
50 private pendingGestures: Record<string, this> = {};
51 private oldState: State = State.UNDETERMINED;
52 private lastSentState: State | null = null;
53 private gestureInstance: number;
54 private _stillWaiting: any;
55 private propsRef: any;
56 private ref: any;
57
58 abstract get name(): string;
59
60 get id() {
61 return `${this.name}${this.gestureInstance}`;
62 }
63
64 get isDiscrete() {
65 return false;
66 }
67
68 get shouldEnableGestureOnSetup(): boolean {
69 throw new Error('Must override GestureHandler.shouldEnableGestureOnSetup');
70 }
71
72 constructor() {
73 this.gestureInstance = gestureInstances++;
74 this.hasCustomActivationCriteria = false;
75 }
76
77 getConfig() {
78 return this.config;
79 }
80
81 onWaitingEnded(_gesture: this) {}
82
83 removePendingGesture(id: string) {
84 delete this.pendingGestures[id];
85 }
86
87 addPendingGesture(gesture: this) {
88 this.pendingGestures[gesture.id] = gesture;
89 }
90
91 isGestureEnabledForEvent(
92 _config: any,
93 _recognizer: any,
94 _event: any
95 ): { failed?: boolean; success?: boolean } {
96 return { success: true };
97 }
98
99 get NativeGestureClass(): RecognizerStatic {
100 throw new Error('Must override GestureHandler.NativeGestureClass');
101 }
102
103 updateHasCustomActivationCriteria(_config: Config) {
104 return true;
105 }
106
107 clearSelfAsPending = () => {
108 if (Array.isArray(this.config.waitFor)) {
109 for (const gesture of this.config.waitFor) {
110 gesture.removePendingGesture(this.id);
111 }
112 }
113 };
114
115 updateGestureConfig({ enabled = true, ...props }) {
116 this.clearSelfAsPending();
117
118 this.config = ensureConfig({ enabled, ...props });
119 this.hasCustomActivationCriteria = this.updateHasCustomActivationCriteria(
120 this.config
121 );
122 if (Array.isArray(this.config.waitFor)) {
123 for (const gesture of this.config.waitFor) {
124 gesture.addPendingGesture(this);
125 }
126 }
127
128 if (this.hammer) {
129 this.sync();
130 }
131 return this.config;
132 }
133
134 destroy = () => {
135 this.clearSelfAsPending();
136
137 if (this.hammer) {
138 this.hammer.stop(false);
139 this.hammer.destroy();
140 }
141 this.hammer = null;
142 };
143
144 isPointInView = ({ x, y }: { x: number; y: number }) => {
145 // @ts-ignore FIXME(TS)
146 const rect = this.view!.getBoundingClientRect();
147 const pointerInside =
148 x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
149 return pointerInside;
150 };
151
152 getState(type: keyof typeof EventMap): State {
153 // @ts-ignore TODO(TS) check if this is needed
154 if (type == 0) {
155 return 0;
156 }
157 return EventMap[type];
158 }
159
160 transformEventData(event: HammerInputExt) {
161 const { eventType, maxPointers: numberOfPointers } = event;
162 // const direction = DirectionMap[ev.direction];
163 const changedTouch = event.changedPointers[0];
164 const pointerInside = this.isPointInView({
165 x: changedTouch.clientX,
166 y: changedTouch.clientY,
167 });
168
169 // TODO(TS) Remove cast after https://github.com/DefinitelyTyped/DefinitelyTyped/pull/50966 is merged.
170 const state = this.getState(eventType as 1 | 2 | 4 | 8);
171 if (state !== this.previousState) {
172 this.oldState = this.previousState;
173 this.previousState = state;
174 }
175
176 return {
177 nativeEvent: {
178 numberOfPointers,
179 state,
180 pointerInside,
181 ...this.transformNativeEvent(event),
182 // onHandlerStateChange only
183 handlerTag: this.handlerTag,
184 target: this.ref,
185 oldState: this.oldState,
186 },
187 timeStamp: Date.now(),
188 };
189 }
190
191 transformNativeEvent(_event: HammerInputExt) {
192 return {};
193 }
194
195 sendEvent = (nativeEvent: HammerInputExt) => {
196 const {
197 onGestureHandlerEvent,
198 onGestureHandlerStateChange,
199 } = this.propsRef.current;
200
201 const event = this.transformEventData(nativeEvent);
202
203 invokeNullableMethod(onGestureHandlerEvent, event);
204 if (this.lastSentState !== event.nativeEvent.state) {
205 this.lastSentState = event.nativeEvent.state as State;
206 invokeNullableMethod(onGestureHandlerStateChange, event);
207 }
208 };
209
210 cancelPendingGestures(event: HammerInputExt) {
211 for (const gesture of Object.values(this.pendingGestures)) {
212 if (gesture && gesture.isGestureRunning) {
213 gesture.hasGestureFailed = true;
214 gesture.cancelEvent(event);
215 }
216 }
217 }
218
219 notifyPendingGestures() {
220 for (const gesture of Object.values(this.pendingGestures)) {
221 if (gesture) {
222 gesture.onWaitingEnded(this);
223 }
224 }
225 }
226
227 // FIXME event is undefined in runtime when firstly invoked (see Draggable example), check other functions taking event as input
228 onGestureEnded(event: HammerInputExt) {
229 this.isGestureRunning = false;
230 this.cancelPendingGestures(event);
231 }
232
233 forceInvalidate(event: HammerInputExt) {
234 if (this.isGestureRunning) {
235 this.hasGestureFailed = true;
236 this.cancelEvent(event);
237 }
238 }
239
240 cancelEvent(event: HammerInputExt) {
241 this.notifyPendingGestures();
242 this.sendEvent({
243 ...event,
244 eventType: Hammer.INPUT_CANCEL,
245 isFinal: true,
246 });
247 this.onGestureEnded(event);
248 }
249
250 onRawEvent({ isFirst }: HammerInputExt) {
251 if (isFirst) {
252 this.hasGestureFailed = false;
253 }
254 }
255
256 setView(ref: Parameters<typeof findNodeHandle>['0'], propsRef: any) {
257 if (ref == null) {
258 this.destroy();
259 this.view = null;
260 return;
261 }
262
263 this.propsRef = propsRef;
264 this.ref = ref;
265
266 this.view = findNodeHandle(ref);
267 this.hammer = new Hammer.Manager(this.view as any);
268
269 this.oldState = State.UNDETERMINED;
270 this.previousState = State.UNDETERMINED;
271 this.lastSentState = null;
272
273 const { NativeGestureClass } = this;
274 // @ts-ignore TODO(TS)
275 const gesture = new NativeGestureClass(this.getHammerConfig());
276 this.hammer.add(gesture);
277
278 this.hammer.on('hammer.input', (ev: HammerInput) => {
279 if (!this.config.enabled) {
280 this.hasGestureFailed = false;
281 this.isGestureRunning = false;
282 return;
283 }
284
285 this.onRawEvent((ev as unknown) as HammerInputExt);
286
287 // TODO: Bacon: Check against something other than null
288 // The isFirst value is not called when the first rotation is calculated.
289 if (this.initialRotation === null && ev.rotation !== 0) {
290 this.initialRotation = ev.rotation;
291 }
292 if (ev.isFinal) {
293 // in favor of a willFail otherwise the last frame of the gesture will be captured.
294 setTimeout(() => {
295 this.initialRotation = null;
296 this.hasGestureFailed = false;
297 });
298 }
299 });
300
301 this.setupEvents();
302 this.sync();
303 }
304
305 setupEvents() {
306 // TODO(TS) Hammer types aren't exactly that what we get in runtime
307 if (!this.isDiscrete) {
308 this.hammer!.on(`${this.name}start`, (event: HammerInput) =>
309 this.onStart((event as unknown) as HammerInputExt)
310 );
311 this.hammer!.on(
312 `${this.name}end ${this.name}cancel`,
313 (event: HammerInput) => {
314 this.onGestureEnded((event as unknown) as HammerInputExt);
315 }
316 );
317 }
318 this.hammer!.on(this.name, (ev: HammerInput) =>
319 this.onGestureActivated((ev as unknown) as HammerInputExt)
320 ); // TODO(TS) remove cast after https://github.com/DefinitelyTyped/DefinitelyTyped/pull/50438 is merged
321 }
322
323 onStart({ deltaX, deltaY, rotation }: HammerInputExt) {
324 // Reset the state for the next gesture
325 this.oldState = State.UNDETERMINED;
326 this.previousState = State.UNDETERMINED;
327 this.lastSentState = null;
328
329 this.isGestureRunning = true;
330 this.__initialX = deltaX;
331 this.__initialY = deltaY;
332 this.initialRotation = rotation;
333 }
334
335 onGestureActivated(ev: HammerInputExt) {
336 this.sendEvent(ev);
337 }
338
339 onSuccess() {}
340
341 _getPendingGestures() {
342 if (Array.isArray(this.config.waitFor) && this.config.waitFor.length) {
343 // Get the list of gestures that this gesture is still waiting for.
344 // Use `=== false` in case a ref that isn't a gesture handler is used.
345 const stillWaiting = this.config.waitFor.filter(
346 ({ hasGestureFailed }) => hasGestureFailed === false
347 );
348 return stillWaiting;
349 }
350 return [];
351 }
352
353 getHammerConfig() {
354 const pointers =
355 this.config.minPointers === this.config.maxPointers
356 ? this.config.minPointers
357 : 0;
358 return {
359 pointers,
360 };
361 }
362
363 sync = () => {
364 const gesture = this.hammer!.get(this.name);
365 if (!gesture) return;
366
367 const enable = (recognizer: any, inputData: any) => {
368 if (!this.config.enabled) {
369 this.isGestureRunning = false;
370 this.hasGestureFailed = false;
371 return false;
372 }
373
374 // Prevent events before the system is ready.
375 if (
376 !inputData ||
377 !recognizer.options ||
378 typeof inputData.maxPointers === 'undefined'
379 ) {
380 return this.shouldEnableGestureOnSetup;
381 }
382
383 if (this.hasGestureFailed) {
384 return false;
385 }
386
387 if (!this.isDiscrete) {
388 if (this.isGestureRunning) {
389 return true;
390 }
391 // The built-in hammer.js "waitFor" doesn't work across multiple views.
392 // Only process if there are views to wait for.
393 this._stillWaiting = this._getPendingGestures();
394 // This gesture should continue waiting.
395 if (this._stillWaiting.length) {
396 // Check to see if one of the gestures you're waiting for has started.
397 // If it has then the gesture should fail.
398 for (const gesture of this._stillWaiting) {
399 // When the target gesture has started, this gesture must force fail.
400 if (!gesture.isDiscrete && gesture.isGestureRunning) {
401 this.hasGestureFailed = true;
402 this.isGestureRunning = false;
403 return false;
404 }
405 }
406 // This gesture shouldn't start until the others have finished.
407 return false;
408 }
409 }
410
411 // Use default behaviour
412 if (!this.hasCustomActivationCriteria) {
413 return true;
414 }
415
416 const deltaRotation =
417 this.initialRotation == null
418 ? 0
419 : inputData.rotation - this.initialRotation;
420 // @ts-ignore FIXME(TS)
421 const { success, failed } = this.isGestureEnabledForEvent(
422 this.getConfig(),
423 recognizer,
424 {
425 ...inputData,
426 deltaRotation,
427 }
428 );
429
430 if (failed) {
431 this.simulateCancelEvent(inputData);
432 this.hasGestureFailed = true;
433 }
434 return success;
435 };
436
437 const params = this.getHammerConfig();
438 // @ts-ignore FIXME(TS)
439 gesture.set({ ...params, enable });
440 };
441
442 simulateCancelEvent(_inputData: any) {}
443}
444
445// TODO(TS) investigate this method
446// Used for sending data to a callback or AnimatedEvent
447function invokeNullableMethod(
448 method:
449 | ((event: NativeEvent) => void)
450 | { __getHandler: () => (event: NativeEvent) => void }
451 | { __nodeConfig: { argMapping: any } },
452 event: NativeEvent
453) {
454 if (method) {
455 if (typeof method === 'function') {
456 method(event);
457 } else {
458 // For use with reanimated's AnimatedEvent
459 if (
460 '__getHandler' in method &&
461 typeof method.__getHandler === 'function'
462 ) {
463 const handler = method.__getHandler();
464 invokeNullableMethod(handler, event);
465 } else {
466 if ('__nodeConfig' in method) {
467 const { argMapping } = method.__nodeConfig;
468 if (Array.isArray(argMapping)) {
469 for (const index in argMapping) {
470 const [key, value] = argMapping[index];
471 if (key in event.nativeEvent) {
472 // @ts-ignore fix method type
473 const nativeValue = event.nativeEvent[key];
474 if (value && value.setValue) {
475 // Reanimated API
476 value.setValue(nativeValue);
477 } else {
478 // RN Animated API
479 method.__nodeConfig.argMapping[index] = [key, nativeValue];
480 }
481 }
482 }
483 }
484 }
485 }
486 }
487 }
488}
489
490// Validate the props
491function ensureConfig(config: Config): Required<Config> {
492 const props = { ...config };
493
494 // TODO(TS) We use ! to assert that if property is present then value is not empty (null, undefined)
495 if ('minDist' in config) {
496 props.minDist = config.minDist;
497 props.minDistSq = props.minDist! * props.minDist!;
498 }
499 if ('minVelocity' in config) {
500 props.minVelocity = config.minVelocity;
501 props.minVelocitySq = props.minVelocity! * props.minVelocity!;
502 }
503 if ('maxDist' in config) {
504 props.maxDist = config.maxDist;
505 props.maxDistSq = config.maxDist! * config.maxDist!;
506 }
507 if ('waitFor' in config) {
508 props.waitFor = asArray(config.waitFor)
509 .map(({ handlerTag }: { handlerTag: number }) =>
510 NodeManager.getHandler(handlerTag)
511 )
512 .filter((v) => v);
513 } else {
514 props.waitFor = null;
515 }
516
517 const configProps = [
518 'minPointers',
519 'maxPointers',
520 'minDist',
521 'maxDist',
522 'maxDistSq',
523 'minVelocitySq',
524 'minDistSq',
525 'minVelocity',
526 'failOffsetXStart',
527 'failOffsetYStart',
528 'failOffsetXEnd',
529 'failOffsetYEnd',
530 'activeOffsetXStart',
531 'activeOffsetXEnd',
532 'activeOffsetYStart',
533 'activeOffsetYEnd',
534 ] as const;
535 configProps.forEach((prop: typeof configProps[number]) => {
536 if (typeof props[prop] === 'undefined') {
537 props[prop] = Number.NaN;
538 }
539 });
540 return props as Required<Config>; // TODO(TS) how to convince TS that props are filled?
541}
542
543function asArray<T>(value: T | T[]) {
544 // TODO(TS) use config.waitFor type
545 return value == null ? [] : Array.isArray(value) ? value : [value];
546}
547
548export default GestureHandler;