InteractionManager.js 6.93 KB
/**
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * @providesModule InteractionManager
 * @flow
 */
'use strict';

const BatchedBridge = require('BatchedBridge');
const EventEmitter = require('EventEmitter');
const Set = require('Set');
const TaskQueue = require('TaskQueue');

const infoLog = require('infoLog');
const invariant = require('fbjs/lib/invariant');
/* $FlowFixMe(>=0.54.0 site=react_native_oss) This comment suppresses an error
 * found when Flow v0.54 was deployed. To see the error delete this comment and
 * run Flow. */
const keyMirror = require('fbjs/lib/keyMirror');

type Handle = number;
import type {Task} from 'TaskQueue';

const _emitter = new EventEmitter();

const DEBUG_DELAY = 0;
const DEBUG = 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.
 */
var 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('create interaction handle');
    _scheduleUpdate();
    var handle = ++_inc;
    _addInteractionSet.add(handle);
    return handle;
  },

  /**
   * Notify manager that an interaction has completed.
   */
  clearInteractionHandle(handle: Handle) {
    DEBUG && infoLog('clear interaction handle');
    invariant(
      !!handle,
      'Must provide a handle to clear.'
    );
    _scheduleUpdate();
    _addInteractionSet.delete(handle);
    _deleteInteractionSet.add(handle);
  },

  addListener: _emitter.addListener.bind(_emitter),

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

  var interactionCount = _interactionSet.size;
  _addInteractionSet.forEach(handle =>
    _interactionSet.add(handle)
  );
  _deleteInteractionSet.forEach(handle =>
    _interactionSet.delete(handle)
  );
  var 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;