123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222 |
- /**
- * 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;
|