This commit is contained in:
Yamozha
2021-04-02 02:24:13 +03:00
parent c23950b545
commit 7256d79e2c
31493 changed files with 3036630 additions and 0 deletions

View File

@ -0,0 +1,76 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow strict-local
*/
'use strict';
const InteractionManager = require('./InteractionManager');
/**
* A simple class for batching up invocations of a low-pri callback. A timeout is set to run the
* callback once after a delay, no matter how many times it's scheduled. Once the delay is reached,
* InteractionManager.runAfterInteractions is used to invoke the callback after any hi-pri
* interactions are done running.
*
* Make sure to cleanup with dispose(). Example:
*
* class Widget extends React.Component {
* _batchedSave: new Batchinator(() => this._saveState, 1000);
* _saveSate() {
* // save this.state to disk
* }
* componentDidUpdate() {
* this._batchedSave.schedule();
* }
* componentWillUnmount() {
* this._batchedSave.dispose();
* }
* ...
* }
*/
class Batchinator {
_callback: () => void;
_delay: number;
_taskHandle: ?{cancel: () => void, ...};
constructor(callback: () => void, delayMS: number) {
this._delay = delayMS;
this._callback = callback;
}
/*
* Cleanup any pending tasks.
*
* By default, if there is a pending task the callback is run immediately. Set the option abort to
* true to not call the callback if it was pending.
*/
dispose(options: {abort: boolean, ...} = {abort: false}) {
if (this._taskHandle) {
this._taskHandle.cancel();
if (!options.abort) {
this._callback();
}
this._taskHandle = null;
}
}
schedule() {
if (this._taskHandle) {
return;
}
const timeoutHandle = setTimeout(() => {
this._taskHandle = InteractionManager.runAfterInteractions(() => {
// Note that we clear the handle before invoking the callback so that if the callback calls
// schedule again, it will actually schedule another task.
this._taskHandle = null;
this._callback();
});
}, this._delay);
this._taskHandle = {cancel: () => clearTimeout(timeoutHandle)};
}
}
module.exports = Batchinator;

View File

@ -0,0 +1,62 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow
*/
'use strict';
const JSEventLoopWatchdog = require('./JSEventLoopWatchdog');
const MessageQueue = require('../BatchedBridge/MessageQueue');
const infoLog = require('../Utilities/infoLog');
const BridgeSpyStallHandler = {
register: function() {
let spyBuffer = [];
MessageQueue.spy(data => {
spyBuffer.push(data);
});
const TO_JS = 0;
JSEventLoopWatchdog.addHandler({
onStall: () => {
infoLog(
spyBuffer.length + ' bridge messages during stall: ',
spyBuffer.map(info => {
let args = '<args>';
try {
args = JSON.stringify(info.args);
} catch (e1) {
if (Array.isArray(info.args)) {
args = info.args.map(arg => {
try {
return JSON.stringify(arg);
} catch (e2) {
return '?';
}
});
} else {
args = 'keys:' + JSON.stringify(Object.keys(info.args));
}
}
return (
`${info.type === TO_JS ? 'N->JS' : 'JS->N'} : ` +
`${info.module ? info.module + '.' : ''}${
info.method
}(${JSON.stringify(args)})`
);
}),
);
},
onIterate: () => {
spyBuffer = [];
},
});
},
};
module.exports = BridgeSpyStallHandler;

View File

@ -0,0 +1,84 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow strict-local
*/
'use strict';
import NativeFrameRateLogger from './NativeFrameRateLogger';
const invariant = require('invariant');
/**
* Flow API for native FrameRateLogger module. If the native module is not installed, function calls
* are just no-ops.
*
* Typical behavior is that `setContext` is called when a new screen is loaded (e.g. via a
* navigation integration), and then `beginScroll` is called by `ScrollResponder` at which point the
* native module then begins tracking frame drops. When `ScrollResponder` calls `endScroll`, the
* native module gathers up all it's frame drop data and reports it via an analytics pipeline for
* analysis.
*
* Note that `beginScroll` may be called multiple times by `ScrollResponder` - unclear if that's a
* bug, but the native module should be robust to that.
*
* In the future we may add support for tracking frame drops in other types of interactions beyond
* scrolling.
*/
const FrameRateLogger = {
/**
* Enable `debug` to see local logs of what's going on. `reportStackTraces` will grab stack traces
* during UI thread stalls and upload them if the native module supports it.
*/
setGlobalOptions: function(options: {
debug?: boolean,
reportStackTraces?: boolean,
...
}) {
if (options.debug !== undefined) {
invariant(
NativeFrameRateLogger,
'Trying to debug FrameRateLogger without the native module!',
);
}
if (NativeFrameRateLogger) {
// Needs to clone the object first to avoid modifying the argument.
const optionsClone = {
debug: !!options.debug,
reportStackTraces: !!options.reportStackTraces,
};
NativeFrameRateLogger.setGlobalOptions(optionsClone);
}
},
/**
* Must call `setContext` before any events can be properly tracked, which is done automatically
* in `AppRegistry`, but navigation is also a common place to hook in.
*/
setContext: function(context: string) {
NativeFrameRateLogger && NativeFrameRateLogger.setContext(context);
},
/**
* Called in `ScrollResponder` so any component that uses that module will handle this
* automatically.
*/
beginScroll() {
NativeFrameRateLogger && NativeFrameRateLogger.beginScroll();
},
/**
* Called in `ScrollResponder` so any component that uses that module will handle this
* automatically.
*/
endScroll() {
NativeFrameRateLogger && NativeFrameRateLogger.endScroll();
},
};
module.exports = FrameRateLogger;

View File

@ -0,0 +1,222 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow
*/
'use strict';
const BatchedBridge = require('../BatchedBridge/BatchedBridge');
const EventEmitter = require('../vendor/emitter/EventEmitter');
const TaskQueue = require('./TaskQueue');
const infoLog = require('../Utilities/infoLog');
const invariant = require('invariant');
const keyMirror = require('fbjs/lib/keyMirror');
export type Handle = number;
import type {Task} from './TaskQueue';
const _emitter = new EventEmitter();
const DEBUG_DELAY: 0 = 0;
const DEBUG: false = false;
/**
* InteractionManager allows long-running work to be scheduled after any
* interactions/animations have completed. In particular, this allows JavaScript
* animations to run smoothly.
*
* Applications can schedule tasks to run after interactions with the following:
*
* ```
* InteractionManager.runAfterInteractions(() => {
* // ...long-running synchronous task...
* });
* ```
*
* Compare this to other scheduling alternatives:
*
* - requestAnimationFrame(): for code that animates a view over time.
* - setImmediate/setTimeout(): run code later, note this may delay animations.
* - runAfterInteractions(): run code later, without delaying active animations.
*
* The touch handling system considers one or more active touches to be an
* 'interaction' and will delay `runAfterInteractions()` callbacks until all
* touches have ended or been cancelled.
*
* InteractionManager also allows applications to register animations by
* creating an interaction 'handle' on animation start, and clearing it upon
* completion:
*
* ```
* var handle = InteractionManager.createInteractionHandle();
* // run animation... (`runAfterInteractions` tasks are queued)
* // later, on animation completion:
* InteractionManager.clearInteractionHandle(handle);
* // queued tasks run if all handles were cleared
* ```
*
* `runAfterInteractions` takes either a plain callback function, or a
* `PromiseTask` object with a `gen` method that returns a `Promise`. If a
* `PromiseTask` is supplied, then it is fully resolved (including asynchronous
* dependencies that also schedule more tasks via `runAfterInteractions`) before
* starting on the next task that might have been queued up synchronously
* earlier.
*
* By default, queued tasks are executed together in a loop in one
* `setImmediate` batch. If `setDeadline` is called with a positive number, then
* tasks will only be executed until the deadline (in terms of js event loop run
* time) approaches, at which point execution will yield via setTimeout,
* allowing events such as touches to start interactions and block queued tasks
* from executing, making apps more responsive.
*/
const InteractionManager = {
Events: keyMirror({
interactionStart: true,
interactionComplete: true,
}),
/**
* Schedule a function to run after all interactions have completed. Returns a cancellable
* "promise".
*/
runAfterInteractions(
task: ?Task,
): {
then: Function,
done: Function,
cancel: Function,
...
} {
const tasks = [];
const promise = new Promise(resolve => {
_scheduleUpdate();
if (task) {
tasks.push(task);
}
tasks.push({
run: resolve,
name: 'resolve ' + ((task && task.name) || '?'),
});
_taskQueue.enqueueTasks(tasks);
});
return {
then: promise.then.bind(promise),
done: (...args) => {
if (promise.done) {
return promise.done(...args);
} else {
console.warn(
'Tried to call done when not supported by current Promise implementation.',
);
}
},
cancel: function() {
_taskQueue.cancelTasks(tasks);
},
};
},
/**
* Notify manager that an interaction has started.
*/
createInteractionHandle(): Handle {
DEBUG && infoLog('InteractionManager: create interaction handle');
_scheduleUpdate();
const handle = ++_inc;
_addInteractionSet.add(handle);
return handle;
},
/**
* Notify manager that an interaction has completed.
*/
clearInteractionHandle(handle: Handle) {
DEBUG && infoLog('InteractionManager: clear interaction handle');
invariant(!!handle, 'InteractionManager: Must provide a handle to clear.');
_scheduleUpdate();
_addInteractionSet.delete(handle);
_deleteInteractionSet.add(handle);
},
addListener: (_emitter.addListener.bind(_emitter): $FlowFixMe),
/**
* A positive number will use setTimeout to schedule any tasks after the
* eventLoopRunningTime hits the deadline value, otherwise all tasks will be
* executed in one setImmediate batch (default).
*/
setDeadline(deadline: number) {
_deadline = deadline;
},
};
const _interactionSet = new Set();
const _addInteractionSet = new Set();
const _deleteInteractionSet = new Set();
const _taskQueue = new TaskQueue({onMoreTasks: _scheduleUpdate});
let _nextUpdateHandle = 0;
let _inc = 0;
let _deadline = -1;
declare function setImmediate(callback: any, ...args: Array<any>): number;
/**
* Schedule an asynchronous update to the interaction state.
*/
function _scheduleUpdate() {
if (!_nextUpdateHandle) {
if (_deadline > 0) {
/* $FlowFixMe(>=0.63.0 site=react_native_fb) This comment suppresses an
* error found when Flow v0.63 was deployed. To see the error delete this
* comment and run Flow. */
_nextUpdateHandle = setTimeout(_processUpdate, 0 + DEBUG_DELAY);
} else {
_nextUpdateHandle = setImmediate(_processUpdate);
}
}
}
/**
* Notify listeners, process queue, etc
*/
function _processUpdate() {
_nextUpdateHandle = 0;
const interactionCount = _interactionSet.size;
_addInteractionSet.forEach(handle => _interactionSet.add(handle));
_deleteInteractionSet.forEach(handle => _interactionSet.delete(handle));
const nextInteractionCount = _interactionSet.size;
if (interactionCount !== 0 && nextInteractionCount === 0) {
// transition from 1+ --> 0 interactions
_emitter.emit(InteractionManager.Events.interactionComplete);
} else if (interactionCount === 0 && nextInteractionCount !== 0) {
// transition from 0 --> 1+ interactions
_emitter.emit(InteractionManager.Events.interactionStart);
}
// process the queue regardless of a transition
if (nextInteractionCount === 0) {
while (_taskQueue.hasTasksToProcess()) {
_taskQueue.processNext();
if (
_deadline > 0 &&
BatchedBridge.getEventLoopRunningTime() >= _deadline
) {
// Hit deadline before processing all tasks, so process more later.
_scheduleUpdate();
break;
}
}
}
_addInteractionSet.clear();
_deleteInteractionSet.clear();
}
module.exports = InteractionManager;

View File

@ -0,0 +1,55 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow
*/
'use strict';
const InteractionManager = require('./InteractionManager');
import {type Handle} from './InteractionManager';
/**
* This mixin provides safe versions of InteractionManager start/end methods
* that ensures `clearInteractionHandle` is always called
* once per start, even if the component is unmounted.
*/
const InteractionMixin = {
componentWillUnmount: function() {
while (this._interactionMixinHandles.length) {
InteractionManager.clearInteractionHandle(
this._interactionMixinHandles.pop(),
);
}
},
_interactionMixinHandles: ([]: Array<number>),
createInteractionHandle: function(): Handle {
const handle = InteractionManager.createInteractionHandle();
this._interactionMixinHandles.push(handle);
return handle;
},
clearInteractionHandle: function(clearHandle: number): void {
InteractionManager.clearInteractionHandle(clearHandle);
this._interactionMixinHandles = this._interactionMixinHandles.filter(
handle => handle !== clearHandle,
);
},
/**
* Schedule work for after all interactions have completed.
*
* @param {function} callback
*/
runAfterInteractions: function(callback: Function): void {
InteractionManager.runAfterInteractions(callback);
},
};
module.exports = InteractionMixin;

View File

@ -0,0 +1,23 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow strict-local
*/
'use strict';
const BridgeSpyStallHandler = require('./BridgeSpyStallHandler');
const JSEventLoopWatchdog = require('./JSEventLoopWatchdog');
const InteractionStallDebugger = {
install(options: {thresholdMS: number, ...}): void {
JSEventLoopWatchdog.install(options);
BridgeSpyStallHandler.register();
},
};
module.exports = InteractionStallDebugger;

View File

@ -0,0 +1,93 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow
*/
'use strict';
const infoLog = require('../Utilities/infoLog');
const performanceNow = require('fbjs/lib/performanceNow');
type Handler = {
onIterate?: () => void,
onStall: (params: {
lastInterval: number,
busyTime: number,
...
}) => ?string,
...
};
/**
* A utility for tracking stalls in the JS event loop that prevent timers and
* other events from being processed in a timely manner.
*
* The "stall" time is defined as the amount of time in access of the acceptable
* threshold, which is typically around 100-200ms. So if the treshold is set to
* 100 and a timer fires 150 ms later than it was scheduled because the event
* loop was tied up, that would be considered a 50ms stall.
*
* By default, logs stall events to the console when installed. Can also be
* queried with `getStats`.
*/
const JSEventLoopWatchdog = {
getStats: function(): Object {
return {stallCount, totalStallTime, longestStall, acceptableBusyTime};
},
reset: function() {
infoLog('JSEventLoopWatchdog: reset');
totalStallTime = 0;
stallCount = 0;
longestStall = 0;
lastInterval = performanceNow();
},
addHandler: function(handler: Handler) {
handlers.push(handler);
},
install: function({thresholdMS}: {thresholdMS: number, ...}) {
acceptableBusyTime = thresholdMS;
if (installed) {
return;
}
installed = true;
lastInterval = performanceNow();
function iteration() {
const now = performanceNow();
const busyTime = now - lastInterval;
if (busyTime >= thresholdMS) {
const stallTime = busyTime - thresholdMS;
stallCount++;
totalStallTime += stallTime;
longestStall = Math.max(longestStall, stallTime);
let msg =
`JSEventLoopWatchdog: JS thread busy for ${busyTime}ms. ` +
`${totalStallTime}ms in ${stallCount} stalls so far. `;
handlers.forEach(handler => {
msg += handler.onStall({lastInterval, busyTime}) || '';
});
infoLog(msg);
}
handlers.forEach(handler => {
handler.onIterate && handler.onIterate();
});
lastInterval = now;
setTimeout(iteration, thresholdMS / 5);
}
iteration();
},
};
let acceptableBusyTime = 0;
let installed = false;
let totalStallTime = 0;
let stallCount = 0;
let longestStall = 0;
let lastInterval = 0;
const handlers: Array<Handler> = [];
module.exports = JSEventLoopWatchdog;

View File

@ -0,0 +1,24 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow strict-local
*/
import type {TurboModule} from '../TurboModule/RCTExport';
import * as TurboModuleRegistry from '../TurboModule/TurboModuleRegistry';
export interface Spec extends TurboModule {
+setGlobalOptions: (options: {|
+debug?: ?boolean,
+reportStackTraces?: ?boolean,
|}) => void;
+setContext: (context: string) => void;
+beginScroll: () => void;
+endScroll: () => void;
}
export default (TurboModuleRegistry.get<Spec>('FrameRateLogger'): ?Spec);

View File

@ -0,0 +1,581 @@
/**
* Copyright (c) Facebook, Inc. and its 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';
const InteractionManager = require('./InteractionManager');
const TouchHistoryMath = require('./TouchHistoryMath');
import type {PressEvent} from '../Types/CoreEventTypes';
const currentCentroidXOfTouchesChangedAfter =
TouchHistoryMath.currentCentroidXOfTouchesChangedAfter;
const currentCentroidYOfTouchesChangedAfter =
TouchHistoryMath.currentCentroidYOfTouchesChangedAfter;
const previousCentroidXOfTouchesChangedAfter =
TouchHistoryMath.previousCentroidXOfTouchesChangedAfter;
const previousCentroidYOfTouchesChangedAfter =
TouchHistoryMath.previousCentroidYOfTouchesChangedAfter;
const currentCentroidX = TouchHistoryMath.currentCentroidX;
const currentCentroidY = TouchHistoryMath.currentCentroidY;
/**
* `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 (
* <View {...this._panResponder.panHandlers} />
* );
* },
*
* ```
*
* ### Working Example
*
* To see it in action, try the
* [PanResponder example in RNTester](https://github.com/facebook/react-native/blob/master/RNTester/js/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 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,
|}>;
const PanResponder = {
/**
*
* A graphical explanation of the touch data flow:
*
* +----------------------------+ +--------------------------------+
* | ResponderTouchHistoryStore | |TouchHistoryMath |
* +----------------------------+ +----------+---------------------+
* |Global store of touchHistory| |Allocation-less math util |
* |including activeness, start | |on touch history (centroids |
* |position, prev/cur position.| |and multitouch movement etc) |
* | | | |
* +----^-----------------------+ +----^---------------------------+
* | |
* | (records relevant history |
* | of touches relevant for |
* | implementing higher level |
* | gestures) |
* | |
* +----+-----------------------+ +----|---------------------------+
* | ResponderEventPlugin | | | Your App/Component |
* +----------------------------+ +----|---------------------------+
* |Negotiates which view gets | Low level | | High level |
* |onResponderMove events. | events w/ | +-+-------+ events w/ |
* |Also records history into | touchHistory| | Pan | multitouch + |
* |ResponderTouchHistoryStore. +---------------->Responder+-----> accumulative|
* +----------------------------+ attached to | | | distance and |
* each event | +---------+ velocity. |
* | |
* | |
* +--------------------------------+
*
*
*
* Gesture that calculates cumulative movement over time in a way that just
* "does the right thing" for multiple touches. The "right thing" is very
* nuanced. When moving two touches in opposite directions, the cumulative
* distance is zero in each dimension. When two touches move in parallel five
* pixels in the same direction, the cumulative distance is five, not ten. If
* two touches start, one moves five in a direction, then stops and the other
* touch moves fives in the same direction, the cumulative distance is ten.
*
* This logic requires a kind of processing of time "clusters" of touch events
* so that two touch moves that essentially occur in parallel but move every
* other frame respectively, are considered part of the same movement.
*
* Explanation of some of the non-obvious fields:
*
* - moveX/moveY: If no move event has been observed, then `(moveX, moveY)` is
* invalid. If a move event has been observed, `(moveX, moveY)` is the
* centroid of the most recently moved "cluster" of active touches.
* (Currently all move have the same timeStamp, but later we should add some
* threshold for what is considered to be "moving"). If a palm is
* accidentally counted as a touch, but a finger is moving greatly, the palm
* will move slightly, but we only want to count the single moving touch.
* - x0/y0: Centroid location (non-cumulative) at the time of becoming
* responder.
* - dx/dy: Cumulative touch distance - not the same thing as sum of each touch
* distance. Accounts for touch moves that are clustered together in time,
* moving the same direction. Only valid when currently responder (otherwise,
* it only represents the drag distance below the threshold).
* - vx/vy: Velocity.
*/
_initializeGestureState(gestureState: GestureState) {
gestureState.moveX = 0;
gestureState.moveY = 0;
gestureState.x0 = 0;
gestureState.y0 = 0;
gestureState.dx = 0;
gestureState.dy = 0;
gestureState.vx = 0;
gestureState.vy = 0;
gestureState.numberActiveTouches = 0;
// All `gestureState` accounts for timeStamps up until:
gestureState._accountsForMovesUpTo = 0;
},
/**
* This is nuanced and is necessary. It is incorrect to continuously take all
* active *and* recently moved touches, find the centroid, and track how that
* result changes over time. Instead, we must take all recently moved
* touches, and calculate how the centroid has changed just for those
* recently moved touches, and append that change to an accumulator. This is
* to (at least) handle the case where the user is moving three fingers, and
* then one of the fingers stops but the other two continue.
*
* This is very different than taking all of the recently moved touches and
* storing their centroid as `dx/dy`. For correctness, we must *accumulate
* changes* in the centroid of recently moved touches.
*
* There is also some nuance with how we handle multiple moved touches in a
* single event. With the way `ReactNativeEventEmitter` dispatches touches as
* individual events, multiple touches generate two 'move' events, each of
* them triggering `onResponderMove`. But with the way `PanResponder` works,
* all of the gesture inference is performed on the first dispatch, since it
* looks at all of the touches (even the ones for which there hasn't been a
* native dispatch yet). Therefore, `PanResponder` does not call
* `onResponderMove` passed the first dispatch. This diverges from the
* typical responder callback pattern (without using `PanResponder`), but
* avoids more dispatches than necessary.
*/
_updateGestureStateOnMove(
gestureState: GestureState,
touchHistory: $PropertyType<PressEvent, 'touchHistory'>,
) {
gestureState.numberActiveTouches = touchHistory.numberActiveTouches;
gestureState.moveX = currentCentroidXOfTouchesChangedAfter(
touchHistory,
gestureState._accountsForMovesUpTo,
);
gestureState.moveY = currentCentroidYOfTouchesChangedAfter(
touchHistory,
gestureState._accountsForMovesUpTo,
);
const movedAfter = gestureState._accountsForMovesUpTo;
const prevX = previousCentroidXOfTouchesChangedAfter(
touchHistory,
movedAfter,
);
const x = currentCentroidXOfTouchesChangedAfter(touchHistory, movedAfter);
const prevY = previousCentroidYOfTouchesChangedAfter(
touchHistory,
movedAfter,
);
const y = currentCentroidYOfTouchesChangedAfter(touchHistory, movedAfter);
const nextDX = gestureState.dx + (x - prevX);
const nextDY = gestureState.dy + (y - prevY);
// TODO: This must be filtered intelligently.
const dt =
touchHistory.mostRecentTimeStamp - gestureState._accountsForMovesUpTo;
gestureState.vx = (nextDX - gestureState.dx) / dt;
gestureState.vy = (nextDY - gestureState.dy) / dt;
gestureState.dx = nextDX;
gestureState.dy = nextDY;
gestureState._accountsForMovesUpTo = touchHistory.mostRecentTimeStamp;
},
/**
* @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,
): $TEMPORARY$object<{|
getInteractionHandle: () => ?number,
panHandlers: $TEMPORARY$object<{|
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,
|}>,
|}> {
const interactionState = {
handle: (null: ?number),
};
const gestureState: GestureState = {
// Useful for debugging
stateID: Math.random(),
moveX: 0,
moveY: 0,
x0: 0,
y0: 0,
dx: 0,
dy: 0,
vx: 0,
vy: 0,
numberActiveTouches: 0,
_accountsForMovesUpTo: 0,
};
const panHandlers = {
onStartShouldSetResponder(event: PressEvent): boolean {
return config.onStartShouldSetPanResponder == null
? false
: config.onStartShouldSetPanResponder(event, gestureState);
},
onMoveShouldSetResponder(event: PressEvent): boolean {
return config.onMoveShouldSetPanResponder == null
? false
: config.onMoveShouldSetPanResponder(event, gestureState);
},
onStartShouldSetResponderCapture(event: PressEvent): boolean {
// TODO: Actually, we should reinitialize the state any time
// touches.length increases from 0 active to > 0 active.
if (event.nativeEvent.touches.length === 1) {
PanResponder._initializeGestureState(gestureState);
}
gestureState.numberActiveTouches =
event.touchHistory.numberActiveTouches;
return config.onStartShouldSetPanResponderCapture != null
? config.onStartShouldSetPanResponderCapture(event, gestureState)
: false;
},
onMoveShouldSetResponderCapture(event: PressEvent): boolean {
const touchHistory = event.touchHistory;
// Responder system incorrectly dispatches should* to current responder
// Filter out any touch moves past the first one - we would have
// already processed multi-touch geometry during the first event.
if (
gestureState._accountsForMovesUpTo ===
touchHistory.mostRecentTimeStamp
) {
return false;
}
PanResponder._updateGestureStateOnMove(gestureState, touchHistory);
return config.onMoveShouldSetPanResponderCapture
? config.onMoveShouldSetPanResponderCapture(event, gestureState)
: false;
},
onResponderGrant(event: PressEvent): boolean {
if (!interactionState.handle) {
interactionState.handle = InteractionManager.createInteractionHandle();
}
gestureState.x0 = currentCentroidX(event.touchHistory);
gestureState.y0 = currentCentroidY(event.touchHistory);
gestureState.dx = 0;
gestureState.dy = 0;
if (config.onPanResponderGrant) {
config.onPanResponderGrant(event, gestureState);
}
// TODO: t7467124 investigate if this can be removed
return config.onShouldBlockNativeResponder == null
? true
: config.onShouldBlockNativeResponder(event, gestureState);
},
onResponderReject(event: PressEvent): void {
clearInteractionHandle(
interactionState,
config.onPanResponderReject,
event,
gestureState,
);
},
onResponderRelease(event: PressEvent): void {
clearInteractionHandle(
interactionState,
config.onPanResponderRelease,
event,
gestureState,
);
PanResponder._initializeGestureState(gestureState);
},
onResponderStart(event: PressEvent): void {
const touchHistory = event.touchHistory;
gestureState.numberActiveTouches = touchHistory.numberActiveTouches;
if (config.onPanResponderStart) {
config.onPanResponderStart(event, gestureState);
}
},
onResponderMove(event: PressEvent): void {
const touchHistory = event.touchHistory;
// Guard against the dispatch of two touch moves when there are two
// simultaneously changed touches.
if (
gestureState._accountsForMovesUpTo ===
touchHistory.mostRecentTimeStamp
) {
return;
}
// Filter out any touch moves past the first one - we would have
// already processed multi-touch geometry during the first event.
PanResponder._updateGestureStateOnMove(gestureState, touchHistory);
if (config.onPanResponderMove) {
config.onPanResponderMove(event, gestureState);
}
},
onResponderEnd(event: PressEvent): void {
const touchHistory = event.touchHistory;
gestureState.numberActiveTouches = touchHistory.numberActiveTouches;
clearInteractionHandle(
interactionState,
config.onPanResponderEnd,
event,
gestureState,
);
},
onResponderTerminate(event: PressEvent): void {
clearInteractionHandle(
interactionState,
config.onPanResponderTerminate,
event,
gestureState,
);
PanResponder._initializeGestureState(gestureState);
},
onResponderTerminationRequest(event: PressEvent): boolean {
return config.onPanResponderTerminationRequest == null
? true
: config.onPanResponderTerminationRequest(event, gestureState);
},
};
return {
panHandlers,
getInteractionHandle(): ?number {
return interactionState.handle;
},
};
},
};
function clearInteractionHandle(
interactionState: {handle: ?number, ...},
callback: ?(ActiveCallback | PassiveCallback),
event: PressEvent,
gestureState: GestureState,
) {
if (interactionState.handle) {
InteractionManager.clearInteractionHandle(interactionState.handle);
interactionState.handle = null;
}
if (callback) {
callback(event, gestureState);
}
}
export type PanResponderInstance = $Call<
$PropertyType<typeof PanResponder, 'create'>,
PanResponderConfig,
>;
module.exports = PanResponder;

View File

@ -0,0 +1,184 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow
*/
'use strict';
const infoLog = require('../Utilities/infoLog');
const invariant = require('invariant');
type SimpleTask = {
name: string,
run: () => void,
...
};
type PromiseTask = {
name: string,
gen: () => Promise<any>,
...
};
export type Task = Function | SimpleTask | PromiseTask;
const DEBUG: false = false;
/**
* TaskQueue - A system for queueing and executing a mix of simple callbacks and
* trees of dependent tasks based on Promises. No tasks are executed unless
* `processNext` is called.
*
* `enqueue` takes a Task object with either a simple `run` callback, or a
* `gen` function that returns a `Promise` and puts it in the queue. If a gen
* function is supplied, then the promise it returns will block execution of
* tasks already in the queue until it resolves. This can be used to make sure
* the first task is fully resolved (including asynchronous dependencies that
* also schedule more tasks via `enqueue`) before starting on the next task.
* The `onMoreTasks` constructor argument is used to inform the owner that an
* async task has resolved and that the queue should be processed again.
*
* Note: Tasks are only actually executed with explicit calls to `processNext`.
*/
class TaskQueue {
/**
* TaskQueue instances are self contained and independent, so multiple tasks
* of varying semantics and priority can operate together.
*
* `onMoreTasks` is invoked when `PromiseTask`s resolve if there are more
* tasks to process.
*/
constructor({onMoreTasks}: {onMoreTasks: () => void, ...}) {
this._onMoreTasks = onMoreTasks;
this._queueStack = [{tasks: [], popable: false}];
}
/**
* Add a task to the queue. It is recommended to name your tasks for easier
* async debugging. Tasks will not be executed until `processNext` is called
* explicitly.
*/
enqueue(task: Task): void {
this._getCurrentQueue().push(task);
}
enqueueTasks(tasks: Array<Task>): void {
tasks.forEach(task => this.enqueue(task));
}
cancelTasks(tasksToCancel: Array<Task>): void {
// search through all tasks and remove them.
this._queueStack = this._queueStack
.map(queue => ({
...queue,
tasks: queue.tasks.filter(task => tasksToCancel.indexOf(task) === -1),
}))
.filter((queue, idx) => queue.tasks.length > 0 || idx === 0);
}
/**
* Check to see if `processNext` should be called.
*
* @returns {boolean} Returns true if there are tasks that are ready to be
* processed with `processNext`, or returns false if there are no more tasks
* to be processed right now, although there may be tasks in the queue that
* are blocked by earlier `PromiseTask`s that haven't resolved yet.
* `onMoreTasks` will be called after each `PromiseTask` resolves if there are
* tasks ready to run at that point.
*/
hasTasksToProcess(): boolean {
return this._getCurrentQueue().length > 0;
}
/**
* Executes the next task in the queue.
*/
processNext(): void {
const queue = this._getCurrentQueue();
if (queue.length) {
const task = queue.shift();
try {
if (task.gen) {
DEBUG && infoLog('TaskQueue: genPromise for task ' + task.name);
this._genPromise((task: any)); // Rather than annoying tagged union
} else if (task.run) {
DEBUG && infoLog('TaskQueue: run task ' + task.name);
task.run();
} else {
invariant(
typeof task === 'function',
'Expected Function, SimpleTask, or PromiseTask, but got:\n' +
JSON.stringify(task, null, 2),
);
DEBUG && infoLog('TaskQueue: run anonymous task');
task();
}
} catch (e) {
e.message =
'TaskQueue: Error with task ' + (task.name || '') + ': ' + e.message;
throw e;
}
}
}
_queueStack: Array<{
tasks: Array<Task>,
popable: boolean,
...
}>;
_onMoreTasks: () => void;
_getCurrentQueue(): Array<Task> {
const stackIdx = this._queueStack.length - 1;
const queue = this._queueStack[stackIdx];
if (
queue.popable &&
queue.tasks.length === 0 &&
this._queueStack.length > 1
) {
this._queueStack.pop();
DEBUG &&
infoLog('TaskQueue: popped queue: ', {
stackIdx,
queueStackSize: this._queueStack.length,
});
return this._getCurrentQueue();
} else {
return queue.tasks;
}
}
_genPromise(task: PromiseTask) {
// Each async task pushes it's own queue onto the queue stack. This
// effectively defers execution of previously queued tasks until the promise
// resolves, at which point we allow the new queue to be popped, which
// happens once it is fully processed.
this._queueStack.push({tasks: [], popable: false});
const stackIdx = this._queueStack.length - 1;
DEBUG && infoLog('TaskQueue: push new queue: ', {stackIdx});
DEBUG && infoLog('TaskQueue: exec gen task ' + task.name);
task
.gen()
.then(() => {
DEBUG &&
infoLog('TaskQueue: onThen for gen task ' + task.name, {
stackIdx,
queueStackSize: this._queueStack.length,
});
this._queueStack[stackIdx].popable = true;
this.hasTasksToProcess() && this._onMoreTasks();
})
.catch(ex => {
ex.message = `TaskQueue: Error resolving Promise in task ${
task.name
}: ${ex.message}`;
throw ex;
})
.done();
}
}
module.exports = TaskQueue;

View File

@ -0,0 +1,153 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
const TouchHistoryMath = {
/**
* This code is optimized and not intended to look beautiful. This allows
* computing of touch centroids that have moved after `touchesChangedAfter`
* timeStamp. You can compute the current centroid involving all touches
* moves after `touchesChangedAfter`, or you can compute the previous
* centroid of all touches that were moved after `touchesChangedAfter`.
*
* @param {TouchHistoryMath} touchHistory Standard Responder touch track
* data.
* @param {number} touchesChangedAfter timeStamp after which moved touches
* are considered "actively moving" - not just "active".
* @param {boolean} isXAxis Consider `x` dimension vs. `y` dimension.
* @param {boolean} ofCurrent Compute current centroid for actively moving
* touches vs. previous centroid of now actively moving touches.
* @return {number} value of centroid in specified dimension.
*/
centroidDimension: function(
touchHistory,
touchesChangedAfter,
isXAxis,
ofCurrent,
) {
const touchBank = touchHistory.touchBank;
let total = 0;
let count = 0;
const oneTouchData =
touchHistory.numberActiveTouches === 1
? touchHistory.touchBank[touchHistory.indexOfSingleActiveTouch]
: null;
if (oneTouchData !== null) {
if (
oneTouchData.touchActive &&
oneTouchData.currentTimeStamp > touchesChangedAfter
) {
total +=
ofCurrent && isXAxis
? oneTouchData.currentPageX
: ofCurrent && !isXAxis
? oneTouchData.currentPageY
: !ofCurrent && isXAxis
? oneTouchData.previousPageX
: oneTouchData.previousPageY;
count = 1;
}
} else {
for (let i = 0; i < touchBank.length; i++) {
const touchTrack = touchBank[i];
if (
touchTrack !== null &&
touchTrack !== undefined &&
touchTrack.touchActive &&
touchTrack.currentTimeStamp >= touchesChangedAfter
) {
let toAdd; // Yuck, program temporarily in invalid state.
if (ofCurrent && isXAxis) {
toAdd = touchTrack.currentPageX;
} else if (ofCurrent && !isXAxis) {
toAdd = touchTrack.currentPageY;
} else if (!ofCurrent && isXAxis) {
toAdd = touchTrack.previousPageX;
} else {
toAdd = touchTrack.previousPageY;
}
total += toAdd;
count++;
}
}
}
return count > 0 ? total / count : TouchHistoryMath.noCentroid;
},
currentCentroidXOfTouchesChangedAfter: function(
touchHistory,
touchesChangedAfter,
) {
return TouchHistoryMath.centroidDimension(
touchHistory,
touchesChangedAfter,
true, // isXAxis
true, // ofCurrent
);
},
currentCentroidYOfTouchesChangedAfter: function(
touchHistory,
touchesChangedAfter,
) {
return TouchHistoryMath.centroidDimension(
touchHistory,
touchesChangedAfter,
false, // isXAxis
true, // ofCurrent
);
},
previousCentroidXOfTouchesChangedAfter: function(
touchHistory,
touchesChangedAfter,
) {
return TouchHistoryMath.centroidDimension(
touchHistory,
touchesChangedAfter,
true, // isXAxis
false, // ofCurrent
);
},
previousCentroidYOfTouchesChangedAfter: function(
touchHistory,
touchesChangedAfter,
) {
return TouchHistoryMath.centroidDimension(
touchHistory,
touchesChangedAfter,
false, // isXAxis
false, // ofCurrent
);
},
currentCentroidX: function(touchHistory) {
return TouchHistoryMath.centroidDimension(
touchHistory,
0, // touchesChangedAfter
true, // isXAxis
true, // ofCurrent
);
},
currentCentroidY: function(touchHistory) {
return TouchHistoryMath.centroidDimension(
touchHistory,
0, // touchesChangedAfter
false, // isXAxis
true, // ofCurrent
);
},
noCentroid: -1,
};
module.exports = TouchHistoryMath;