InteractionManager.js 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222
  1. /**
  2. * Copyright (c) Facebook, Inc. and its 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. * @format
  8. * @flow
  9. */
  10. 'use strict';
  11. const BatchedBridge = require('../BatchedBridge/BatchedBridge');
  12. const EventEmitter = require('../vendor/emitter/EventEmitter');
  13. const TaskQueue = require('./TaskQueue');
  14. const infoLog = require('../Utilities/infoLog');
  15. const invariant = require('invariant');
  16. const keyMirror = require('fbjs/lib/keyMirror');
  17. export type Handle = number;
  18. import type {Task} from './TaskQueue';
  19. const _emitter = new EventEmitter();
  20. const DEBUG_DELAY: 0 = 0;
  21. const DEBUG: false = false;
  22. /**
  23. * InteractionManager allows long-running work to be scheduled after any
  24. * interactions/animations have completed. In particular, this allows JavaScript
  25. * animations to run smoothly.
  26. *
  27. * Applications can schedule tasks to run after interactions with the following:
  28. *
  29. * ```
  30. * InteractionManager.runAfterInteractions(() => {
  31. * // ...long-running synchronous task...
  32. * });
  33. * ```
  34. *
  35. * Compare this to other scheduling alternatives:
  36. *
  37. * - requestAnimationFrame(): for code that animates a view over time.
  38. * - setImmediate/setTimeout(): run code later, note this may delay animations.
  39. * - runAfterInteractions(): run code later, without delaying active animations.
  40. *
  41. * The touch handling system considers one or more active touches to be an
  42. * 'interaction' and will delay `runAfterInteractions()` callbacks until all
  43. * touches have ended or been cancelled.
  44. *
  45. * InteractionManager also allows applications to register animations by
  46. * creating an interaction 'handle' on animation start, and clearing it upon
  47. * completion:
  48. *
  49. * ```
  50. * var handle = InteractionManager.createInteractionHandle();
  51. * // run animation... (`runAfterInteractions` tasks are queued)
  52. * // later, on animation completion:
  53. * InteractionManager.clearInteractionHandle(handle);
  54. * // queued tasks run if all handles were cleared
  55. * ```
  56. *
  57. * `runAfterInteractions` takes either a plain callback function, or a
  58. * `PromiseTask` object with a `gen` method that returns a `Promise`. If a
  59. * `PromiseTask` is supplied, then it is fully resolved (including asynchronous
  60. * dependencies that also schedule more tasks via `runAfterInteractions`) before
  61. * starting on the next task that might have been queued up synchronously
  62. * earlier.
  63. *
  64. * By default, queued tasks are executed together in a loop in one
  65. * `setImmediate` batch. If `setDeadline` is called with a positive number, then
  66. * tasks will only be executed until the deadline (in terms of js event loop run
  67. * time) approaches, at which point execution will yield via setTimeout,
  68. * allowing events such as touches to start interactions and block queued tasks
  69. * from executing, making apps more responsive.
  70. */
  71. const InteractionManager = {
  72. Events: keyMirror({
  73. interactionStart: true,
  74. interactionComplete: true,
  75. }),
  76. /**
  77. * Schedule a function to run after all interactions have completed. Returns a cancellable
  78. * "promise".
  79. */
  80. runAfterInteractions(
  81. task: ?Task,
  82. ): {
  83. then: Function,
  84. done: Function,
  85. cancel: Function,
  86. ...
  87. } {
  88. const tasks = [];
  89. const promise = new Promise(resolve => {
  90. _scheduleUpdate();
  91. if (task) {
  92. tasks.push(task);
  93. }
  94. tasks.push({
  95. run: resolve,
  96. name: 'resolve ' + ((task && task.name) || '?'),
  97. });
  98. _taskQueue.enqueueTasks(tasks);
  99. });
  100. return {
  101. then: promise.then.bind(promise),
  102. done: (...args) => {
  103. if (promise.done) {
  104. return promise.done(...args);
  105. } else {
  106. console.warn(
  107. 'Tried to call done when not supported by current Promise implementation.',
  108. );
  109. }
  110. },
  111. cancel: function() {
  112. _taskQueue.cancelTasks(tasks);
  113. },
  114. };
  115. },
  116. /**
  117. * Notify manager that an interaction has started.
  118. */
  119. createInteractionHandle(): Handle {
  120. DEBUG && infoLog('InteractionManager: create interaction handle');
  121. _scheduleUpdate();
  122. const handle = ++_inc;
  123. _addInteractionSet.add(handle);
  124. return handle;
  125. },
  126. /**
  127. * Notify manager that an interaction has completed.
  128. */
  129. clearInteractionHandle(handle: Handle) {
  130. DEBUG && infoLog('InteractionManager: clear interaction handle');
  131. invariant(!!handle, 'InteractionManager: Must provide a handle to clear.');
  132. _scheduleUpdate();
  133. _addInteractionSet.delete(handle);
  134. _deleteInteractionSet.add(handle);
  135. },
  136. addListener: (_emitter.addListener.bind(_emitter): $FlowFixMe),
  137. /**
  138. * A positive number will use setTimeout to schedule any tasks after the
  139. * eventLoopRunningTime hits the deadline value, otherwise all tasks will be
  140. * executed in one setImmediate batch (default).
  141. */
  142. setDeadline(deadline: number) {
  143. _deadline = deadline;
  144. },
  145. };
  146. const _interactionSet = new Set();
  147. const _addInteractionSet = new Set();
  148. const _deleteInteractionSet = new Set();
  149. const _taskQueue = new TaskQueue({onMoreTasks: _scheduleUpdate});
  150. let _nextUpdateHandle = 0;
  151. let _inc = 0;
  152. let _deadline = -1;
  153. declare function setImmediate(callback: any, ...args: Array<any>): number;
  154. /**
  155. * Schedule an asynchronous update to the interaction state.
  156. */
  157. function _scheduleUpdate() {
  158. if (!_nextUpdateHandle) {
  159. if (_deadline > 0) {
  160. /* $FlowFixMe(>=0.63.0 site=react_native_fb) This comment suppresses an
  161. * error found when Flow v0.63 was deployed. To see the error delete this
  162. * comment and run Flow. */
  163. _nextUpdateHandle = setTimeout(_processUpdate, 0 + DEBUG_DELAY);
  164. } else {
  165. _nextUpdateHandle = setImmediate(_processUpdate);
  166. }
  167. }
  168. }
  169. /**
  170. * Notify listeners, process queue, etc
  171. */
  172. function _processUpdate() {
  173. _nextUpdateHandle = 0;
  174. const interactionCount = _interactionSet.size;
  175. _addInteractionSet.forEach(handle => _interactionSet.add(handle));
  176. _deleteInteractionSet.forEach(handle => _interactionSet.delete(handle));
  177. const nextInteractionCount = _interactionSet.size;
  178. if (interactionCount !== 0 && nextInteractionCount === 0) {
  179. // transition from 1+ --> 0 interactions
  180. _emitter.emit(InteractionManager.Events.interactionComplete);
  181. } else if (interactionCount === 0 && nextInteractionCount !== 0) {
  182. // transition from 0 --> 1+ interactions
  183. _emitter.emit(InteractionManager.Events.interactionStart);
  184. }
  185. // process the queue regardless of a transition
  186. if (nextInteractionCount === 0) {
  187. while (_taskQueue.hasTasksToProcess()) {
  188. _taskQueue.processNext();
  189. if (
  190. _deadline > 0 &&
  191. BatchedBridge.getEventLoopRunningTime() >= _deadline
  192. ) {
  193. // Hit deadline before processing all tasks, so process more later.
  194. _scheduleUpdate();
  195. break;
  196. }
  197. }
  198. }
  199. _addInteractionSet.clear();
  200. _deleteInteractionSet.clear();
  201. }
  202. module.exports = InteractionManager;